diff --git a/.eslintignore b/.eslintignore index 1a460ffea4b..1290df50503 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ lib node_modules standalone templates +.firebase diff --git a/.eslintrc.js b/.eslintrc.js index aedec3be694..7addbda6455 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,6 @@ module.exports = { "plugin:jsdoc/recommended", "google", "prettier", - "prettier/@typescript-eslint", ], rules: { "jsdoc/newline-after-description": "off", @@ -37,8 +36,12 @@ module.exports = { "jsdoc/require-param": "off", "jsdoc/require-returns": "off", + "@typescript-eslint/no-invalid-this": "error", + "@typescript-eslint/no-unused-vars": "error", // Unused vars should not exist. "no-invalid-this": "off", // Turned off in favor of @typescript-eslint/no-invalid-this. - "@typescript-eslint/no-invalid-this": ["error"], + "no-unused-vars": "off", // Off in favor of @typescript-eslint/no-unused-vars. + eqeqeq: ["error", "always", { null: "ignore" }], + camelcase: ["error", { properties: "never" }], // snake_case allowed in properties iif to satisfy an external contract / style "@typescript-eslint/ban-types": "warn", // TODO(bkendall): remove, allow to error. "@typescript-eslint/explicit-function-return-type": ["warn", { allowExpressions: true }], // TODO(bkendall): SET to error. @@ -47,6 +50,7 @@ module.exports = { "@typescript-eslint/no-inferrable-types": "warn", // TODO(bkendall): remove, allow to error. "@typescript-eslint/no-misused-promises": "warn", // TODO(bkendall): remove, allow to error. "@typescript-eslint/no-unnecessary-type-assertion": "warn", // TODO(bkendall): remove, allow to error. + "@typescript-eslint/no-unsafe-argument": "warn", // TODO(bkendall): remove, allow to error. "@typescript-eslint/no-unsafe-assignment": "warn", // TODO(bkendall): remove, allow to error. "@typescript-eslint/no-unsafe-call": "warn", // TODO(bkendall): remove, allow to error. "@typescript-eslint/no-unsafe-member-access": "warn", // TODO(bkendall): remove, allow to error. @@ -61,8 +65,6 @@ module.exports = { "no-case-declarations": "warn", // TODO(bkendall): remove, allow to error. "no-constant-condition": "warn", // TODO(bkendall): remove, allow to error. "no-fallthrough": "warn", // TODO(bkendall): remove, allow to error. - "no-unused-vars": "warn", // TODO(bkendall): remove, allow to error. - camelcase: ["warn", { ignoreDestructuring: true }], // TODO(bkendall): remove, allow to error. }, }, { @@ -73,6 +75,7 @@ module.exports = { "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", @@ -113,4 +116,21 @@ module.exports = { }, }, parser: "@typescript-eslint/parser", + // dynamicImport.js is skipped in the tsbuild, we inject it manually since we + // don't want Typescript to turn the imports into requires. Ignoring as eslint + // is complaining it doesn't belong to a project. + // TODO(jamesdaniels): add this to overrides instead + ignorePatterns: [ + "src/dynamicImport.js", + "scripts/webframeworks-deploy-tests/nextjs/**", + "scripts/webframeworks-deploy-tests/angular/**", + "scripts/frameworks-tests/vite-project/**", + "/src/frameworks/docs/**", + // This file is taking a very long time to lint, 2-4m + "src/emulator/auth/schema.ts", + // TODO(hsubox76): Set up a job to run eslint separately on vscode dir + "firebase-vscode/", + // If this is leftover from "clean-install.sh", don't lint it + "clean/**", + ], }; diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f6cccad084a..a17f7d1811b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,8 +1,8 @@ --- -name: ⚠️ Bug report +name: "⚠️ Bug report" about: Create a report to help us improve title: "" -labels: bug +labels: "type: bug" assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 3b3f1dd9c1f..c58daaf219e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,8 +1,8 @@ --- -name: 💡 Feature request +name: "\U0001F4A1 Feature request" about: Suggest an idea for this project title: "" -labels: feature request +labels: "type: feature request" assignees: "" --- diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..55159aa1457 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + groups: + patch: + update-types: + - "patch" + minor: + update-types: + - "minor" diff --git a/.github/workflows/functions.yaml b/.github/workflows/functions.yaml new file mode 100644 index 00000000000..2815ff8aef7 --- /dev/null +++ b/.github/workflows/functions.yaml @@ -0,0 +1,43 @@ +name: Functions deploy test + +# Allow workflow to be triggered manually. +# https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow +on: + workflow_dispatch: +# schedule: +# # Run the action every 2 hours. +# # * is a special character in YAML so you have to quote this string +# - cron: "0 */2 * * *" + +permissions: + contents: read + +concurrency: + # Limit at most 1 runs + group: functions-deploy-${{ github.ref }} + +env: + CI: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "16" + + - uses: google-github-actions/auth@v0 + with: + credentials_json: "${{ secrets.CF3_INTEGRATION_TEST_GOOGLE_CREDENTIALS }}" + create_credentials_file: true + + - run: npm ci + + - name: "Test function deploy" + run: npm run test:functions-deploy + + - name: Print debug logs + if: failure() + run: find . -type f -name "*debug.log" | xargs cat diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index a41713ebcdc..d595a1da61c 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -3,102 +3,142 @@ name: CI Tests on: - pull_request - push + - merge_group env: CI: true + NO_COLOR: true + +permissions: + contents: read + +concurrency: + # node-test-[pull_request|push]-[branch], typically. Will cancel previous runs that match! + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + if: contains(fromJSON('["pull_request", "merge_group"]'), github.event_name) strategy: matrix: node-version: - - 12.x + - "20" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: npm-shrinkwrap.json + + - run: npm i -g npm@9.5 + - run: npm ci + - run: npm run lint:changed-files + - run: npm run lint + working-directory: firebase-vscode - - name: Cache npm - uses: actions/cache@v2 + vscode_unit: + # Using windows to bypass an error thrown by VScode + # when run in an environment that does not have a screen. + runs-on: windows-latest + strategy: + matrix: + node-version: + - "18" + - "20" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: firebase-vscode/package-lock.json + - run: npm i -g npm@9.5 - run: npm ci - - run: npm run lint:changed-files + - run: npm install + working-directory: firebase-vscode + - run: npm run test:unit + working-directory: firebase-vscode + + - uses: codecov/codecov-action@v3 + if: matrix.node-version == '20' unit: runs-on: ubuntu-latest strategy: matrix: node-version: - - 12.x - - 14.x - - 16.x + - "18" + - "20" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: npm-shrinkwrap.json - - name: Cache npm - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} - + - run: npm i -g npm@9.5 - run: npm ci - - run: npm test + - run: npm test -- -- --forbid-only + + - uses: codecov/codecov-action@v3 + if: matrix.node-version == '20' integration: needs: unit - if: github.event_name == 'push' + if: contains(fromJSON('["push", "merge_group"]'), github.event_name) runs-on: ubuntu-latest env: - FIREBASE_CLI_PREVIEWS: none FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache COMMIT_SHA: ${{ github.sha }} CI_JOB_ID: ${{ github.action }} FBTOOLS_TARGET_PROJECT: ${{ secrets.FBTOOLS_TARGET_PROJECT }} FBTOOLS_CLIENT_INTEGRATION_SITE: ${{ secrets.FBTOOLS_CLIENT_INTEGRATION_SITE }} + CI_RUN_ID: ${{ github.run_id }} + CI_RUN_ATTEMPT: ${{ github.run_attempt }} strategy: fail-fast: false matrix: node-version: - - 12.x + - "20" script: - - npm run test:hosting - npm run test:client-integration - npm run test:emulator - npm run test:extensions-emulator - - npm run test:triggers-end-to-end + - npm run test:frameworks + - npm run test:functions-discover + - npm run test:hosting + # - npm run test:hosting-rewrites # Long-running test that might conflict across test runs. Run this manually. + - npm run test:import-export + - npm run test:storage-deploy - npm run test:storage-emulator-integration + - npm run test:triggers-end-to-end + - npm run test:triggers-end-to-end:inspect + - npm run test:dataconnect-deploy steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - - - name: Cache npm - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} + cache: npm + cache-dependency-path: npm-shrinkwrap.json - name: Cache firebase emulators - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.FIREBASE_EMULATORS_PATH }} key: ${{ runner.os }}-firebase-emulators-${{ hashFiles('emulator-cache/**') }} continue-on-error: true + - run: npm i -g npm@9.5 - run: npm ci - run: echo ${{ secrets.service_account_json_base64 }} | base64 -d > ./scripts/service-account.json - run: ${{ matrix.script }} @@ -107,22 +147,105 @@ jobs: if: failure() run: find . -type f -name "*debug.log" | xargs cat + integration-windows: + needs: unit + if: contains(fromJSON('["push", "merge_group"]'), github.event_name) + runs-on: windows-latest + + env: + FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache + COMMIT_SHA: ${{ github.sha }} + CI_JOB_ID: ${{ github.action }} + FBTOOLS_TARGET_PROJECT: ${{ secrets.FBTOOLS_TARGET_PROJECT }} + FBTOOLS_CLIENT_INTEGRATION_SITE: ${{ secrets.FBTOOLS_CLIENT_INTEGRATION_SITE }} + CI_RUN_ID: ${{ github.run_id }} + CI_RUN_ATTEMPT: ${{ github.run_attempt }} + + strategy: + fail-fast: false + matrix: + node-version: + - "20" + script: + - npm run test:hosting + # - npm run test:hosting-rewrites # Long-running test that might conflict across test runs. Run this manually. + - npm run test:client-integration + - npm run test:emulator + # - npm run test:import-export # Fails becuase port 4000 is taken after first run - hub not shutting down? + # - npm run test:extensions-emulator # Fails due to cannot find module sharp (not waiting for npm install?) + - npm run test:functions-discover + # - npm run test:triggers-end-to-end + - npm run test:triggers-end-to-end:inspect + - npm run test:storage-deploy + # - npm run test:storage-emulator-integration + - npm run test:dataconnect-deploy + steps: + - name: Setup Java JDK + uses: actions/setup-java@v3.3.0 + with: + java-version: 17 + distribution: temurin + + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: npm-shrinkwrap.json + + - name: Cache firebase emulators + uses: actions/cache@v3 + with: + path: ${{ env.FIREBASE_EMULATORS_PATH }} + key: ${{ runner.os }}-firebase-emulators-${{ hashFiles('emulator-cache/**') }} + continue-on-error: true + + - run: echo ${{ secrets.service_account_json_base64 }} > tmp.txt + - run: certutil -decode tmp.txt scripts/service-account.json + - run: npm i -g npm@9.5 + - run: npm ci + - run: ${{ matrix.script }} + - name: Print debug logs + if: failure() + run: type *debug.log + check-package-lock: runs-on: ubuntu-latest strategy: matrix: node-version: - - 12.x + - "20" + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm i -g npm@9.5 + # --ignore-scripts prevents the `prepare` script from being run. + - run: npm install --package-lock-only --ignore-scripts + - run: "git diff --exit-code -- npm-shrinkwrap.json || (echo 'Error: npm-shrinkwrap.json is changed during npm install! Please make sure to use npm >= 8 and commit npm-shrinkwrap.json.' && false)" + + check-package-lock-vsce: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: + - "20" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - - run: npm install --package-lock-only - - run: "git diff --exit-code -- package-lock.json || (echo 'Error: package-lock.json is changed during npm install! Please make sure to use npm >= 6.9.0 and commit package-lock.json.' && false)" + - run: npm i -g npm@9.5 + # --ignore-scripts prevents the `prepare` script from being run. + - run: "(cd firebase-vscode && npm install --package-lock-only --ignore-scripts)" + - run: "git diff --exit-code -- firebase-vscode/package-lock.json || (echo 'Error: firebase-vscode/package-lock.json is changed during npm install! Please make sure to use npm >= 8 and commit firebase-vscode/package-lock.json.' && false)" check-json-schema: runs-on: ubuntu-latest @@ -130,14 +253,16 @@ jobs: strategy: matrix: node-version: - - 12.x + - "20" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: npm-shrinkwrap.json - run: npm install - run: npm run generate:json-schema - run: "git diff --exit-code -- schema/*.json || (echo 'Error: JSON schema is changed! Please run npm run generate:json-schema and commit the results.' && false)" diff --git a/.gitignore b/.gitignore index 9852be09c6a..6799d4b5ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/node_modules/* +src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/package.json +src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/package-lock.json + /.vscode node_modules /coverage @@ -6,6 +10,7 @@ node_modules firebase-debug.log firebase-debug.*.log npm-debug.log +ui-debug.log yarn.lock .npmrc @@ -17,3 +22,4 @@ scripts/*.json lib/ dev/ +clean/ diff --git a/.prettierignore b/.prettierignore index 60f523e5f03..6e89334f915 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,10 @@ /node_modules /lib/**/* /CONTRIBUTING.md +/scripts/frameworks-tests/vite-project/** +/scripts/webframeworks-deploy-tests/angular/** +/scripts/webframeworks-deploy-tests/nextjs/** +/src/frameworks/docs/** + +# Intentionally invalid YAML file: +/src/test/fixtures/extension-yamls/invalid/extension.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9cc9751d8..bb9105ddb4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1 @@ -- **BREAKING** Drops support for running the CLI on Node 10. -- **BREAKING** Replaces all usages of `-y`, `--yes`, or `--confirm` with `-f` and `--force`. -- **BREAKING** Function deploys upload source to the deployed region instead of us-central1. -- Requires firebase-functions >= 3.13.1 in Functions emulator to include bug fixes (#3851). -- Updates default functions runtime to Node.js 16. +- Fixes framework support for Nuxt ^3.12 by correctly calling loadNuxtConfig() (#7375) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92eb719f4cb..b779aa98a5a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,15 +148,10 @@ are unavailable to Pull Requests coming from forks of the repository. | path | description | | --------------- | --------------------------------------------------------- | | `src` | Contains shared/support code for the commands | -| `src/bin` | Contains the runnable script. You shouldn't need to touch | -: : this content. : -| `src/commands` | Contains code for the commands, organized by | -: : one-file-per-command with dashes. : -| `src/templates` | Contains static files needed for various reasons | -: : (inittemplates, login success HTML, etc.) : -| `src/test` | Contains tests. Mirrors the top-level directory structure | -: : (i.e., `src/test/commands` contains command tests and : -: : `src/test/gcp` contains `gcp` tests) : +| `src/bin` | Contains the runnable script. You shouldn't need to touch this content. | +| `src/commands` | Contains code for the commands, organized by one-file-per-command with dashes. | +| `src/test` | Contains test helpers. Actual tests (`*.spec.ts`) should be colocated with source files. | +| `templates` | Contains static files needed for various reasons (init templates, login success HTML, etc.) | ## Building CLI commands @@ -176,7 +171,7 @@ colons with dashes where appropriate. Populate the file with this basic content: import { Command } from "../command"; // `export default` is used for consistency in command files. -export default new Command("your:command") +export const command = new Command("your:command") .description("a one-line description of your command") // .option("-e, --example ", "describe the option briefly") // .before(requireConfig) // add any necessary filters and require them above @@ -277,14 +272,14 @@ logger.info("This text will be displayed to the end user."); logger.debug("This text will only show up in firebase-debug.log or running with --debug."); ``` -In addition, the [cli-color](https://www.npmjs.com/package/cli-color) Node.js +In addition, the [colorette](https://www.npmjs.com/package/colorette) Node.js library should be used for color/formatting of the output: ```typescript -import * as clc "cli-color"; +import { green, bold, underline } from "colorette"; // Generally, prefer template strings (using `backticks`), but this is a formatting example: -const out = "Formatting is " + clc.bold.underline("fun") + " and " + clc.green("easy") + "."; +const out = "Formatting is " + bold(underline("fun")) + " and " + green("easy") + "."; ``` Colors will automatically be stripped from environments that do not support @@ -306,14 +301,14 @@ file), throw a `FirebaseError` with a friendly error message. The original error may be provided as well. Here's an example: ```typescript -import * as clc from "cli-color"; +import { bold } from "colorette"; import { FirebaseError } from "../error"; -async function myFunc(options: any): void { +async function myFunc(projectId: string): void { try { - return await somethingThatMayFail(options.projectId); - } catch (err) { - throw FirebaseError(`Project ${clc.bold(projectId)} caused an issue.', { original: err }); + return await somethingThatMayFail(projectId); + } catch (err: any) { + throw FirebaseError(`Project ${bold(projectId)} caused an issue.', { original: err }); } } ``` diff --git a/README.md b/README.md index 32b716c5f0c..a62f2479bcd 100644 --- a/README.md +++ b/README.md @@ -136,15 +136,21 @@ Detailed doc is [here](https://firebase.google.com/docs/cli/auth). ### Cloud Functions Commands -| Command | Description | -| -------------------------- | ------------------------------------------------------------------------------------------------------------ | -| **functions:log** | Read logs from deployed Cloud Functions. | -| **functions:config:set** | Store runtime configuration values for the current project's Cloud Functions. | -| **functions:config:get** | Retrieve existing configuration values for the current project's Cloud Functions. | -| **functions:config:unset** | Remove values from the current project's runtime configuration. | -| **functions:config:clone** | Copy runtime configuration from one project environment to another. | -| **functions:delete** | Delete one or more Cloud Functions by name or group name. | -| **functions:shell** | Locally emulate functions and start Node.js shell where these local functions can be invoked with test data. | +| Command | Description | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | +| **functions:log** | Read logs from deployed Cloud Functions. | +| **functions:list** | List all deployed functions in your Firebase project. | +| **functions:config:set** | Store runtime configuration values for the current project's Cloud Functions. | +| **functions:config:get** | Retrieve existing configuration values for the current project's Cloud Functions. | +| **functions:config:unset** | Remove values from the current project's runtime configuration. | +| **functions:config:clone** | Copy runtime configuration from one project environment to another. | +| **functions:secrets:set** | Create or update a secret for use in Cloud Functions for Firebase. | +| **functions:secrets:get** | Get metadata for secret and its versions. | +| **functions:secrets:access** | Access secret value given secret and its version. Defaults to accessing the latest version. | +| **functions:secrets:prune** | Destroys unused secrets. | +| **functions:secrets:destroy** | Destroy a secret. Defaults to destroying the latest version. | +| **functions:delete** | Delete one or more Cloud Functions by name or group name. | +| **functions:shell** | Locally emulate functions and start Node.js shell where these local functions can be invoked with test data. | ### Hosting Commands @@ -168,11 +174,11 @@ Use `firebase:deploy --only remoteconfig` to update and publish a project's Fire The Firebase CLI can use one of four authentication methods listed in descending priority: -- **User Token** - provide an explicit long-lived Firebase user token generated from `firebase login:ci`. Note that these tokens are extremely sensitive long-lived credentials and are not the right option for most cases. Consider using service account authorization instead. The token can be set in one of two ways: +- **User Token** - **DEPRECATED: this authentication method will be removed in a future major version of `firebase-tools`; use a service account to authenticate instead** - provide an explicit long-lived Firebase user token generated from `firebase login:ci`. Note that these tokens are extremely sensitive long-lived credentials and are not the right option for most cases. Consider using service account authorization instead. The token can be set in one of two ways: - Set the `--token` flag on any command, for example `firebase --token="" projects:list`. - Set the `FIREBASE_TOKEN` environment variable. - **Local Login** - run `firebase login` to log in to the CLI directly as yourself. The CLI will cache an authorized user credential on your machine. -- **Service Account** - set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to point to the path of a JSON service account key file. +- **Service Account** - set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to point to the path of a JSON service account key file. For more details, see Google Cloud's [Getting started with authentication](https://cloud.google.com/docs/authentication/getting-started) guide. - **Application Default Credentials** - if you use the `gcloud` CLI and log in with `gcloud auth application-default login`, the Firebase CLI will use them if none of the above credentials are present. ### Multiple Accounts @@ -218,21 +224,14 @@ or `HTTP_PROXY` value in your environment to the URL of your proxy (e.g. The Firebase CLI requires a browser to complete authentication, but is fully compatible with CI and other headless environments. -1. On a machine with a browser, install the Firebase CLI. -2. Run `firebase login:ci` to log in and print out a new [refresh token](https://developers.google.com/identity/protocols/OAuth2) - (the current CLI session will not be affected). -3. Store the output token in a secure but accessible way in your CI system. +Complete the following steps to run Firebase commands in a CI environment. Find detailed instructions for each step in Google Cloud's [Getting started with authentication](https://cloud.google.com/docs/authentication/getting-started) guide. -There are two ways to use this token when running Firebase commands: +1. Create a service account and grant it the appropriate level of access to your project. +1. Create a service account key (JSON file) for that service account. +1. Store the key file in a secure, accessible way in your CI system. +1. Set `GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json` in your CI system when running Firebase commands. -1. Store the token as the environment variable `FIREBASE_TOKEN` and it will - automatically be utilized. -2. Run all commands with the `--token ` flag in your CI system. - -The order of precedence for token loading is flag, environment variable, active project. - -On any machine with the Firebase CLI, running `firebase logout --token ` -will immediately revoke access for the specified token. +To disable access for the service account, [find the service account](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts) for your project in the Google Cloud Console, and then either remove the key, or disable or delete the service account. ## Using as a Module diff --git a/firebase-vscode/.eslintrc.json b/firebase-vscode/.eslintrc.json new file mode 100644 index 00000000000..566c5f681c1 --- /dev/null +++ b/firebase-vscode/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + // "react" + ], + "rules": { + "@typescript-eslint/semi": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-throw-literal": "warn", + "semi": "off" + }, + "ignorePatterns": ["out", "dist", "**/*.d.ts"] +} diff --git a/firebase-vscode/.gitignore b/firebase-vscode/.gitignore new file mode 100644 index 00000000000..c974aeb9293 --- /dev/null +++ b/firebase-vscode/.gitignore @@ -0,0 +1,8 @@ +*.vsix +dist/ +*.scss.d.ts +resources/dist +.vscode-test +.wdio-vscode-service +logs +!*.tgz \ No newline at end of file diff --git a/firebase-vscode/.prettierignore b/firebase-vscode/.prettierignore new file mode 100644 index 00000000000..284acd000df --- /dev/null +++ b/firebase-vscode/.prettierignore @@ -0,0 +1,10 @@ +## The default +**/.git +**/.svn +**/.hg +**/node_modules + +## The good stuff +dist +resources +package-lock.json \ No newline at end of file diff --git a/firebase-vscode/.prettierrc.js b/firebase-vscode/.prettierrc.js new file mode 100644 index 00000000000..e83d7e4ded7 --- /dev/null +++ b/firebase-vscode/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + printWidth: 80, +}; diff --git a/firebase-vscode/.vscode/extensions.json b/firebase-vscode/.vscode/extensions.json new file mode 100644 index 00000000000..c0a2258b02c --- /dev/null +++ b/firebase-vscode/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/firebase-vscode/.vscode/launch.json b/firebase-vscode/.vscode/launch.json new file mode 100644 index 00000000000..42cc083538d --- /dev/null +++ b/firebase-vscode/.vscode/launch.json @@ -0,0 +1,31 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.3", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "VSCODE_DEBUG_MODE": "true" + } + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/dist/test/suite/index" + ], + "outFiles": ["${workspaceFolder}/dist/test/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/firebase-vscode/.vscode/settings.json b/firebase-vscode/.vscode/settings.json new file mode 100644 index 00000000000..10d3a97a91d --- /dev/null +++ b/firebase-vscode/.vscode/settings.json @@ -0,0 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "dist": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "dist": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} diff --git a/firebase-vscode/.vscode/tasks.json b/firebase-vscode/.vscode/tasks.json new file mode 100644 index 00000000000..74418433df5 --- /dev/null +++ b/firebase-vscode/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": ["$tsc-watch", "$ts-webpack-watch"], + "isBackground": true, + "presentation": { + "reveal": "always" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/firebase-vscode/.vscodeignore b/firebase-vscode/.vscodeignore new file mode 100644 index 00000000000..43d19f15c1a --- /dev/null +++ b/firebase-vscode/.vscodeignore @@ -0,0 +1,23 @@ +.vscode/** +.vscode-test/** +src/** +webviews/** +common/** +extension/** +public/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts +*.vsix +webpack.*.js +../ +*.zip +node_modules/ +dist/test/ +*.tgz +package-lock.json +.wdio-vscode-service/ \ No newline at end of file diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md new file mode 100644 index 00000000000..a83760644e9 --- /dev/null +++ b/firebase-vscode/CHANGELOG.md @@ -0,0 +1,131 @@ +## NEXT + +## 0.3.0 + +- Updated internal firebase-tools dependency to 13.12.0 + +## 0.2.9 + +- Updated internal firebase-tools dependency to 13.11.4 + +- Support CLI started emulators + +## 0.2.8 + +- Updated internal firebase-tools dependency to 13.11.3 + +## 0.2.7 + +- Updated internal firebase-tools dependency to 13.11.2 + +## 0.2.6 + +- Updated internal firebase-tools dependency to 13.11.1 + +- Fix behaviour on failed postgres connection + +## 0.2.5 + +- Icon fix + +## 0.2.4 + +- Emulator bump v1.2.0 +- Connect to postgres flow reworked +- Telemetry enabled + +## 0.2.3 + +- Emulator bump v1.1.19 + +## 0.2.2 + +- Emulator bump v1.1.18 + +## 0.2.1 + +- Update Logo +- Improve init flow for users with existing firebase.json + +## 0.2.0 + +- Fix Auth on IDX + +## 0.1.9 + +- Fix "Add Data" for nonnull and custom keys +- Emulator Bump 1.1.17 + +## 0.1.8 + +- Update Extensions page Logo +- Update README for Extensions page +- Surface emulator issues as notifications +- Generate .graphqlrc automatically +- Emulator Bump 1.1.16 + +## 0.1.7 + +- Emulator Bump 1.1.14 + +## 0.1.6 + +- Fix deploy command + +## 0.1.5 + +- Fix authentication issues for Introspection and local executions + +## 0.1.4 + +- Dataconnect Sidebar UI refresh + - Emulator and Production sections + - Separate Deploy All and Deploy individual buttons + - Links to external documentation + +## 0.1.0 + +- Data Connect Support + +## 0.0.25 (unreleased) + +- Replace predeploy hack with something more robust. + +## 0.0.24 + +- Remove proxy-agent stub #6172 +- Pull in latest CLI changes (July 25, 2023) + +## 0.0.24-alpha.1 + +- Add more user-friendly API permission denied messages + +## 0.0.24-alpha.0 + +- Remove Google sign-in option from Monospace. +- Fix some Google sign-in login/logout logic. +- Prioritize showing latest failed deploy if it failed. + +## 0.0.23 (July 13, 2023) + +- Same as alpha.5, marked as stable. + +## 0.0.23-alpha.5 (July 12, 2023) + +- More fixes for service account detection logic. +- Better output and error handling for init and deploy steps. + +## 0.0.23-alpha.4 (July 11, 2023) + +- Fix for service account bugs + +## 0.0.23-alpha.3 (July 7, 2023) + +- UX updates + - Get last deploy date + - Write to and show info level logs in output channel when deploying + - Enable user to view service account email + +## 0.0.23-alpha.2 (July 6 2023) + +- Service account internal fixes plus fix for deploying Next.js twice in a row diff --git a/firebase-vscode/CONTRIBUTING.md b/firebase-vscode/CONTRIBUTING.md new file mode 100644 index 00000000000..f9852244fb4 --- /dev/null +++ b/firebase-vscode/CONTRIBUTING.md @@ -0,0 +1,79 @@ +## Setting up the repository + +We use `npm` as package manager. +Run `npm i` in both the parent folder and this folder: + +```sh +cd .. +npm i +cd firebase-vscode +npm i +``` + +## Running tests + +### Unit tests + +Unit tests are located in `src/test/suite`. +The path to the test file should match the path to the source file. For example: `src/core/index.ts` should have its test located at `src/test/suite/core/index.test.ts` + +They can be run with `npm run test:unit`. + +#### Mocking dependencies inside unit tests + +There is currently no support for stubbing imports. + +If you wish to mock a functionality for a given test, you will need to introduce a layer of indirection for that feature. Then, your tests +would be able to replace the implementation with a different one. + +For instance, say you wanted to mock `vscode.workspace`: +Instead of using `vscode.workspace` directly in the extension, you could +create an object that encapsulate `vscode.workspace`: + +```ts +export const workspace = { + value: vscode.workspace, +}; +``` + +You would then use `workspace.value` in the extension. And then, +when it comes to writing your test, you'd be able to change `workspace.value`: + +```ts +it("description", () => { + workspace.value = + /* whatever */ + /* Now run the code you want to test */ + assert.equal(something, somethingElse); + + /* Now reset the value back to normal */ + workspace.value = vscode.workspace; +}); +``` + +Of course, doing this by hand is error prone. It's easy to forget to reset +a value back to normal. + +To help with that, some testing utilities were made. +Using them, your test would instead look like: + +```ts +// A wrapper around `it` +firebaseTest("description", () => { + mock(workspace /* whatever */); + + /* Now run the code you want to test */ + assert.equal(something, somethingElse); + + /* No need to reset values. `mock` automatically handles this. */ +}); +``` + +### Integration tests + +E2e tests can be found at `src/test/integration`. +To run them, use: + +```sh +npm run test:e2e +``` diff --git a/firebase-vscode/LICENSE b/firebase-vscode/LICENSE new file mode 100644 index 00000000000..3da21db2851 --- /dev/null +++ b/firebase-vscode/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Firebase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/firebase-vscode/README.md b/firebase-vscode/README.md new file mode 100644 index 00000000000..0e93b223f9e --- /dev/null +++ b/firebase-vscode/README.md @@ -0,0 +1,12 @@ +# Firebase Extension + +VSCode extension for Firebase. The Firebase Extension currently supports the Data Connect product. + +## Data Connect features + +- Inline CodeLenses allow for one-click execution of operations + - Pass in arguments + - Impersonate user authentication +- Generate queries and mutations from your schema with one click +- Run against the emulator for offline development +- Deploy and execute against production diff --git a/firebase-vscode/README_DEV.md b/firebase-vscode/README_DEV.md new file mode 100644 index 00000000000..abaea3ca54f --- /dev/null +++ b/firebase-vscode/README_DEV.md @@ -0,0 +1,67 @@ +# firebase-vscode README + +This extension is in the development and exploration stage. + +## Running + +1. In order to make sure f5 launches the extension properly, first open your + VS Code session from the `firebase-vscode` subdirectory (not the `firebase-tools` directory). +2. npm i (run this in both `firebase-tools` and `firebase-vscode`) +3. Make sure the extension `amodio.tsl-problem-matcher` is installed - this + enables the watcher to work, otherwise the Extension Development Host + will not automatically open on F5 when the compilation is done. +4. f5 to run opens new window + f5 -> npm run watch defined in tasks.json + My terminal didn't have npm available but yours might + +Workaround if f5 doesnt work: + +1. Execute `npm run watch` from within the vscode directory + Aside: Running `npm run watch` or `npm run build` the extension is compiled into dist (extension.js) + Changing code within extension is hot-reloaded + Modifying extensions.js will not hot-reload + source file src/extension.ts +2. Wait for completion +3. Hit play from the left nav + +New code changes are automatically rebuilt if you have `watch` running, however the new VSCode Plugin-enabled window will not reflect changes until reloaded. +Manual reload from new window: "Developer: Reload Window" Default hotkey: cmd + R + +The communication between UI and extension done via the broker (see webview.postMessage) +Web view uses react (carry-over from the hackweek project courtesy of Roman and Prakhar) + +## Structure + +Extention.ts main entry point, calls sidebar.ts and workflow.ts +sidebar.ts loads the UI from the webviews folder +workflow.ts is the driving component (logic source) +cli.ts wraps CLI methods, importing from firebase-tools/src + +When workflow.ts needs to execute some CLI command, it defers to cli.ts + +## State + +currentOptions maintains the currentState of the plugin and is passed as a whole object to populate calls to the firebase-tools methods +`prepare` in the command includes a lot of + +## Logic + +Calling firebase-tools in general follows the stuff: + +1. instead of calling `before`, call `requireAuth` instead + requireAuth is a prerequisite for the plugin UI, needed + Zero-state (before login) directs the user to sign in with google (using firebase-tools CLI) +2. prepare is an implicit command in the cmd class +3. action + +requireAuth -> login with service account or check that you're already logged in via firebase-tools + +## Open issues + +Login changes in the CLI are not immediately reflected in the Plugin, requires restart +If logged-out in the middle of a plugin session, handle requireAuth errors gracefully +Plugin startup is flaky sometimes +Unit/Integration tests are not developed +Code cleanliness/structure TODOs +tsconfig.json's rootDirs includes ["src", "../src", "common"] which causes some issues with import autocomplete +Three package.jsons - one for monospace and one for the standalone plugin, and then root to copy the correct version diff --git a/firebase-vscode/common/error.ts b/firebase-vscode/common/error.ts new file mode 100644 index 00000000000..93d826faa79 --- /dev/null +++ b/firebase-vscode/common/error.ts @@ -0,0 +1,28 @@ +/** An error thrown before the GraphQL operation could complete. + * + * This could include HTTP errors or JSON parsing errors. + */ +export class DataConnectError extends Error { + constructor(message: string, cause?: unknown) { + super(message, { cause }); + } +} + +/** Encode an error into a {@link SerializedError} */ +export function toSerializedError(error: Error): SerializedError { + return { + name: error.name, + message: error.message, + stack: error.stack, + cause: + error.cause instanceof Error ? toSerializedError(error.cause) : undefined, + }; +} + +/** An error object that can be sent across webview boundaries */ +export interface SerializedError { + name?: string; + message: string; + stack?: string; + cause?: SerializedError; +} diff --git a/firebase-vscode/common/graphql.ts b/firebase-vscode/common/graphql.ts new file mode 100644 index 00000000000..38d03a72c32 --- /dev/null +++ b/firebase-vscode/common/graphql.ts @@ -0,0 +1,63 @@ +import { ExecutionResult, GraphQLError } from "graphql"; + +/** Asserts that an unknown object is a {@link ExecutionResult} */ +export function assertExecutionResult( + response: any +): asserts response is ExecutionResult { + if (!response) { + throw new Error(`Expected ExecutionResult but got ${response}`); + } + + const type = typeof response; + if (type !== "object") { + throw new Error(`Expected ExecutionResult but got ${type}`); + } + + const { data, errors } = response; + if (!data && !errors) { + throw new Error( + `Expected ExecutionResult to have either "data" or "errors" set but none found` + ); + } + + if (errors) { + if (!Array.isArray(errors)) { + throw new Error( + `Expected errors to be an array but got ${typeof errors}` + ); + } + for (const error of errors) { + assertGraphQLError(error); + } + } +} + +export function isExecutionResult(response: any): response is ExecutionResult { + try { + assertExecutionResult(response); + return true; + } catch { + return false; + } +} + +/** Asserts that an unknown object is a {@link GraphQLError} */ +export function assertGraphQLError( + error: unknown +): asserts error is GraphQLError { + if (!error) { + throw new Error(`Expected GraphQLError but got ${error}`); + } + + const type = typeof error; + if (type !== "object") { + throw new Error(`Expected GraphQLError but got ${type}`); + } + + const { message } = error as GraphQLError; + if (typeof message !== "string") { + throw new Error( + `Expected GraphQLError to have "message" set but got ${typeof message}` + ); + } +} diff --git a/firebase-vscode/common/messaging/broker.ts b/firebase-vscode/common/messaging/broker.ts new file mode 100644 index 00000000000..c200a58a800 --- /dev/null +++ b/firebase-vscode/common/messaging/broker.ts @@ -0,0 +1,102 @@ +import { MessageParamsMap } from "./protocol"; +import { Listener, Message, MessageListeners } from "./types"; +import { Webview } from "vscode"; + +const isObject = (val: any): boolean => typeof val === "object" && val !== null; + +export type Receiver = {} | Webview; + +export abstract class Broker< + OutgoingMessages extends MessageParamsMap, + IncomingMessages extends MessageParamsMap, + R extends Receiver +> { + protected readonly listeners: MessageListeners = {}; + + abstract sendMessage( + message: T, + data: OutgoingMessages[T] + ): void; + registerReceiver(receiver: R): void {} + + addListener(message: string, cb: Listener): () => void { + const messageListeners = (this.listeners[message] ??= []); + + messageListeners.push(cb); + + return () => { + const index = messageListeners.indexOf(cb); + if (index !== -1) { + messageListeners.splice(index, 1); + } + + if (messageListeners.length === 0) { + delete this.listeners[message]; + } + }; + } + + executeListeners(message: Message) { + if (message === undefined || !isObject(message) || !message.command) { + return; + } + + const d = message; + + if (this.listeners[d.command] === undefined) { + return; + } + + for (const listener of this.listeners[d.command]) { + d.data === undefined ? listener() : listener(d.data); + } + } + + delete(): void {} +} + +export interface BrokerImpl< + OutgoingMessages, + IncomingMessages, + R extends Receiver +> { + send( + message: E, + args?: OutgoingMessages[E] + ): void; + registerReceiver(receiver: R): void; + on( + message: Extract, + listener: (params: IncomingMessages[E]) => void + ): () => void; + delete(): void; +} + +export function createBroker< + OutgoingMessages extends MessageParamsMap, + IncomingMessages extends MessageParamsMap, + R extends Receiver +>( + broker: Broker +): BrokerImpl { + return { + send( + message: Extract, + args?: OutgoingMessages[E] + ): void { + broker.sendMessage(message, args); + }, + registerReceiver(receiver: R): void { + broker.registerReceiver(receiver); + }, + on( + message: Extract, + listener: (params: IncomingMessages[E]) => void + ): () => void { + return broker.addListener(message, listener); + }, + delete(): void { + broker.delete(); + }, + }; +} diff --git a/firebase-vscode/common/messaging/protocol.ts b/firebase-vscode/common/messaging/protocol.ts new file mode 100644 index 00000000000..c2c9aa1c60e --- /dev/null +++ b/firebase-vscode/common/messaging/protocol.ts @@ -0,0 +1,167 @@ +/** + * @fileoverview Lists all possible messages that can be passed back and forth + * between two environments (VScode and Webview) + */ + +import { FirebaseConfig } from "../../../src/firebaseConfig"; +import { User } from "../../../src/types/auth"; +import { ServiceAccountUser } from "../types"; +import { RCData } from "../../../src/rc"; +import { EmulatorUiSelections, RunningEmulatorInfo } from "./types"; +import { ExecutionResult } from "graphql"; +import { SerializedError } from "../error"; + +export const DEFAULT_EMULATOR_UI_SELECTIONS: EmulatorUiSelections = { + projectId: "demo-something", + importStateFolderPath: "", + exportStateOnExit: false, + mode: "dataconnect", + debugLogging: false, +}; + +export enum UserMockKind { + ADMIN = "admin", + UNAUTHENTICATED = "unauthenticated", + AUTHENTICATED = "authenticated", +} +export type UserMock = + | { kind: UserMockKind.ADMIN | UserMockKind.UNAUTHENTICATED } + | { + kind: UserMockKind.AUTHENTICATED; + claims: string; + }; + +export interface WebviewToExtensionParamsMap { + /** + * Ask extension for initial data + */ + getInitialData: {}; + getInitialHasFdcConfigs: void; + + addUser: {}; + logout: { email: string }; + + /* Emulator panel requests */ + getEmulatorUiSelections: void; + getEmulatorInfos: void; + updateEmulatorUiSelections: Partial; + + /** Notify extension that current user has been changed in UI. */ + requestChangeUser: { user: User | ServiceAccountUser }; + + /** Trigger project selection */ + selectProject: {}; + + /** + * Prompt user for text input + */ + promptUserForInput: { title: string; prompt: string }; + + /** Calls the `firebase init` CLI */ + runFirebaseInit: void; + + /** + * Show a UI message using the vscode interface + */ + showMessage: { msg: string; options?: {} }; + + /** + * Write a log to the extension logger. + */ + writeLog: { level: string; args: string[] }; + + /** + * Call extension runtime to open a link (a href does not work in Monospace) + */ + openLink: { + href: string; + }; + + connectToPostgres: void; + disconnectPostgres: void; + getInitialIsConnectedToPostgres: void; + + selectEmulatorImportFolder: {}; + + definedDataConnectArgs: string; + + /** Prompts the user to select a directory in which to place the quickstart */ + chooseQuickstartDir: {}; + + notifyAuthUserMockChange: UserMock; + + /** Deploy connectors/services to production */ + "fdc.deploy": void; + + /** Deploy all connectors/services to production */ + "fdc.deploy-all": void; + + // Initialize "result" tab. + getDataConnectResults: void; + + // execute terminal tasks + executeLogin: void; +} + +export interface DataConnectResults { + query: string; + displayName: string; + results?: ExecutionResult | SerializedError; + args?: string; +} + +export type ValueOrError = + | { value: T; error: undefined } + | { error: string; value: undefined }; + +export interface ExtensionToWebviewParamsMap { + /** Triggered when the emulator UI/state changes */ + notifyEmulatorUiSelectionsChanged: EmulatorUiSelections; + notifyEmulatorStateChanged: { + status: "running" | "stopped" | "starting" | "stopping"; + infos: RunningEmulatorInfo | undefined; + }; + notifyEmulatorImportFolder: { folder: string }; + + notifyIsConnectedToPostgres: boolean; + + notifyPostgresStringChanged: string; + + /** Triggered when new environment variables values are found. */ + notifyEnv: { env: { isMonospace: boolean } }; + + /** Triggered when users have been updated. */ + notifyUsers: { users: User[] }; + + /** Triggered when a new project is selected */ + notifyProjectChanged: { projectId: string }; + + /** + * This can potentially call multiple webviews to notify of user selection. + */ + notifyUserChanged: { user: User | ServiceAccountUser }; + + /** + * Notify webview of initial discovery or change in firebase.json or + * .firebaserc + */ + notifyFirebaseConfig: { + firebaseJson: ValueOrError | undefined; + firebaseRC: ValueOrError | undefined; + }; + /** Whether any dataconnect.yaml is present */ + notifyHasFdcConfigs: boolean; + + /** + * Return user-selected preview channel name + */ + notifyPreviewChannelResponse: { id: string }; + + // data connect specific + notifyDataConnectResults: DataConnectResults; + notifyDataConnectRequiredArgs: { args: string[] }; +} + +export type MessageParamsMap = + | WebviewToExtensionParamsMap + | ExtensionToWebviewParamsMap; diff --git a/firebase-vscode/common/messaging/types.d.ts b/firebase-vscode/common/messaging/types.d.ts new file mode 100644 index 00000000000..3fdc0393d0b --- /dev/null +++ b/firebase-vscode/common/messaging/types.d.ts @@ -0,0 +1,29 @@ +import { EmulatorInfo } from "../emulator/types"; +import { ExtensionToWebviewParamsMap, MessageParamsMap } from "./protocol"; + +export interface Message { + command: string; + data: M[keyof M]; +} + +export type Listener = (args?: M[keyof M]) => void; + +export interface MessageListeners { + [message: string]: Listener[]; +} + +/** + * Info to display in the UI while the emulators are running + */ +export interface RunningEmulatorInfo { + displayInfo: EmulatorInfo[]; +} + +export interface EmulatorUiSelections { + projectId: string; + firebaseJsonPath?: string; + importStateFolderPath?: string; + exportStateOnExit: boolean; + mode: "all" | "dataconnect"; + debugLogging: boolean; +} diff --git a/firebase-vscode/common/types.d.ts b/firebase-vscode/common/types.d.ts new file mode 100644 index 00000000000..076dffb25e2 --- /dev/null +++ b/firebase-vscode/common/types.d.ts @@ -0,0 +1,8 @@ +export interface ServiceAccount { + user: ServiceAccountUser; +} + +export interface ServiceAccountUser { + email: string; + type: "service_account"; +} diff --git a/firebase-vscode/graphql-language-service-5.2.0.tgz b/firebase-vscode/graphql-language-service-5.2.0.tgz new file mode 100644 index 00000000000..cd9b847871a Binary files /dev/null and b/firebase-vscode/graphql-language-service-5.2.0.tgz differ diff --git a/firebase-vscode/graphql-language-service-server-2.12.0.tgz b/firebase-vscode/graphql-language-service-server-2.12.0.tgz new file mode 100644 index 00000000000..7d8bf353716 Binary files /dev/null and b/firebase-vscode/graphql-language-service-server-2.12.0.tgz differ diff --git a/firebase-vscode/package-lock.json b/firebase-vscode/package-lock.json new file mode 100644 index 00000000000..2ed063cc883 --- /dev/null +++ b/firebase-vscode/package-lock.json @@ -0,0 +1,14130 @@ +{ + "name": "firebase-vscode", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "firebase-vscode", + "version": "0.3.0", + "dependencies": { + "@preact/signals-core": "^1.4.0", + "@preact/signals-react": "1.3.6", + "@vscode/codicons": "0.0.30", + "@vscode/vsce": "^2.25.0", + "@vscode/webview-ui-toolkit": "^1.2.1", + "classnames": "^2.3.2", + "exponential-backoff": "3.1.1", + "graphql-language-service": "file:graphql-language-service-5.2.0.tgz", + "graphql-language-service-server": "file:graphql-language-service-server-2.12.0.tgz", + "js-yaml": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vscode-languageclient": "8.1.0" + }, + "devDependencies": { + "@teamsupercell/typings-for-css-modules-loader": "^2.5.1", + "@types/glob": "^8.0.0", + "@types/mocha": "^10.0.1", + "@types/node": "16.x", + "@types/react": "^18.0.9", + "@types/react-dom": "^18.0.4", + "@types/vscode": "^1.69.0", + "@typescript-eslint/eslint-plugin": "^5.45.0", + "@typescript-eslint/parser": "^5.45.0", + "@vscode/test-electron": "^2.2.0", + "@wdio/cli": "^8.27.1", + "@wdio/local-runner": "^8.27.0", + "@wdio/mocha-framework": "^8.27.0", + "@wdio/spec-reporter": "^8.27.0", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.7.1", + "eslint": "^8.28.0", + "eslint-plugin-react": "^7.32.2", + "fork-ts-checker-webpack-plugin": "^7.3.0", + "glob": "^8.0.3", + "graphql": "^16.7.1", + "mini-css-extract-plugin": "^2.6.0", + "mocha": "^10.1.0", + "node-loader": "2.0.0", + "postcss-loader": "^7.0.0", + "prettier": "^3.1.1", + "sass": "^1.52.0", + "sass-loader": "^13.0.0", + "string-replace-loader": "^3.1.0", + "ts-loader": "^9.4.2", + "typescript": "^4.9.3", + "wdio-vscode-service": "^5.2.2", + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1", + "webpack-merge": "^5.8.0" + }, + "engines": { + "vscode": "^1.69.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ardatan/sync-fetch": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz", + "integrity": "sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==", + "dependencies": { + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ardatan/sync-fetch/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==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/cors": { + "version": "8.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/send": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "2.0.0", + "mime": "^3.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "6.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^1.0.0", + "@fastify/send": "^2.0.0", + "content-disposition": "^0.5.3", + "fastify-plugin": "^4.0.0", + "glob": "^8.0.1", + "p-limit": "^3.1.0" + } + }, + "node_modules/@graphql-tools/batch-execute": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.4.tgz", + "integrity": "sha512-kkebDLXgDrep5Y0gK1RN3DMUlLqNhg60OAz0lTCqrYeja6DshxLtLkj+zV4mVbBA4mQOEoBmw6g1LZs3dA84/w==", + "dependencies": { + "@graphql-tools/utils": "^10.0.13", + "dataloader": "^2.2.2", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/code-file-loader": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.0.3.tgz", + "integrity": "sha512-gVnnlWs0Ua+5FkuHHEriFUOI3OIbHv6DS1utxf28n6NkfGMJldC4j0xlJRY0LS6dWK34IGYgD4HelKYz2l8KiA==", + "dependencies": { + "@graphql-tools/graphql-tag-pluck": "8.1.0", + "@graphql-tools/utils": "^10.0.0", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/delegate": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.0.4.tgz", + "integrity": "sha512-WswZRbQZMh/ebhc8zSomK9DIh6Pd5KbuiMsyiKkKz37TWTrlCOe+4C/fyrBFez30ksq6oFyCeSKMwfrCbeGo0Q==", + "dependencies": { + "@graphql-tools/batch-execute": "^9.0.4", + "@graphql-tools/executor": "^1.2.1", + "@graphql-tools/schema": "^10.0.3", + "@graphql-tools/utils": "^10.0.13", + "dataloader": "^2.2.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.2.6.tgz", + "integrity": "sha512-+1kjfqzM5T2R+dCw7F4vdJ3CqG+fY/LYJyhNiWEFtq0ToLwYzR/KKyD8YuzTirEjSxWTVlcBh7endkx5n5F6ew==", + "dependencies": { + "@graphql-tools/utils": "^10.1.1", + "@graphql-typed-document-node/core": "3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.1.2.tgz", + "integrity": "sha512-+9ZK0rychTH1LUv4iZqJ4ESbmULJMTsv3XlFooPUngpxZkk00q6LqHKJRrsLErmQrVaC7cwQCaRBJa0teK17Lg==", + "dependencies": { + "@graphql-tools/utils": "^10.0.13", + "@types/ws": "^8.0.0", + "graphql-ws": "^5.14.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-http": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.0.9.tgz", + "integrity": "sha512-+NXaZd2MWbbrWHqU4EhXcrDbogeiCDmEbrAN+rMn4Nu2okDjn2MTFDbTIab87oEubQCH4Te1wDkWPKrzXup7+Q==", + "dependencies": { + "@graphql-tools/utils": "^10.0.13", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/fetch": "^0.9.0", + "extract-files": "^11.0.0", + "meros": "^1.2.1", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-legacy-ws": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.0.6.tgz", + "integrity": "sha512-lDSxz9VyyquOrvSuCCnld3256Hmd+QI2lkmkEv7d4mdzkxkK4ddAWW1geQiWrQvWmdsmcnGGlZ7gDGbhEExwqg==", + "dependencies": { + "@graphql-tools/utils": "^10.0.13", + "@types/ws": "^8.0.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "ws": "^8.15.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-file-loader": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.1.tgz", + "integrity": "sha512-7gswMqWBabTSmqbaNyWSmRRpStWlcCkBc73E6NZNlh4YNuiyKOwbvSkOUYFOqFMfEL+cFsXgAvr87Vz4XrYSbA==", + "dependencies": { + "@graphql-tools/import": "7.0.1", + "@graphql-tools/utils": "^10.0.13", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.1.0.tgz", + "integrity": "sha512-kt5l6H/7QxQcIaewInTcune6NpATojdFEW98/8xWcgmy7dgXx5vU9e0AicFZIH+ewGyZzTpwFqO2RI03roxj2w==", + "dependencies": { + "@babel/core": "^7.22.9", + "@babel/parser": "^7.16.8", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8", + "@graphql-tools/utils": "^10.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/import": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.1.tgz", + "integrity": "sha512-935uAjAS8UAeXThqHfYVr4HEAp6nHJ2sximZKO1RzUTq5WoALMAhhGARl0+ecm6X+cqNUwIChJbjtaa6P/ML0w==", + "dependencies": { + "@graphql-tools/utils": "^10.0.13", + "resolve-from": "5.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/json-file-loader": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.1.tgz", + "integrity": "sha512-lAy2VqxDAHjVyqeJonCP6TUemrpYdDuKt25a10X6zY2Yn3iFYGnuIDQ64cv3ytyGY6KPyPB+Kp+ZfOkNDG3FQA==", + "dependencies": { + "@graphql-tools/utils": "^10.0.13", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/load": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.0.2.tgz", + "integrity": "sha512-S+E/cmyVmJ3CuCNfDuNF2EyovTwdWfQScXv/2gmvJOti2rGD8jTt9GYVzXaxhblLivQR9sBUCNZu/w7j7aXUCA==", + "dependencies": { + "@graphql-tools/schema": "^10.0.3", + "@graphql-tools/utils": "^10.0.13", + "p-limit": "3.1.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/merge": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.3.tgz", + "integrity": "sha512-FeKv9lKLMwqDu0pQjPpF59GY3HReUkWXKsMIuMuJQOKh9BETu7zPEFUELvcw8w+lwZkl4ileJsHXC9+AnsT2Lw==", + "dependencies": { + "@graphql-tools/utils": "^10.0.13", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.3.tgz", + "integrity": "sha512-p28Oh9EcOna6i0yLaCFOnkcBDQECVf3SCexT6ktb86QNj9idnkhI+tCxnwZDh58Qvjd2nURdkbevvoZkvxzCog==", + "dependencies": { + "@graphql-tools/merge": "^9.0.3", + "@graphql-tools/utils": "^10.0.13", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/url-loader": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.2.tgz", + "integrity": "sha512-1dKp2K8UuFn7DFo1qX5c1cyazQv2h2ICwA9esHblEqCYrgf69Nk8N7SODmsfWg94OEaI74IqMoM12t7eIGwFzQ==", + "dependencies": { + "@ardatan/sync-fetch": "^0.0.1", + "@graphql-tools/delegate": "^10.0.4", + "@graphql-tools/executor-graphql-ws": "^1.1.2", + "@graphql-tools/executor-http": "^1.0.9", + "@graphql-tools/executor-legacy-ws": "^1.0.6", + "@graphql-tools/utils": "^10.0.13", + "@graphql-tools/wrap": "^10.0.2", + "@types/ws": "^8.0.0", + "@whatwg-node/fetch": "^0.9.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.11", + "ws": "^8.12.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.1.3.tgz", + "integrity": "sha512-loco2ctrrMQzdpSHbcOo6+Ecp21BV67cQ2pNGhuVKAexruu01RdLn3LgtK47B9BpLz3cUD6U0u1R0rur7xMOOg==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "cross-inspect": "1.0.0", + "dset": "^3.1.2", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/wrap": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.0.5.tgz", + "integrity": "sha512-Cbr5aYjr3HkwdPvetZp1cpDWTGdD1Owgsb3z/ClzhmrboiK86EnQDxDvOJiQkDCPWE9lNBwj8Y4HfxroY0D9DQ==", + "dependencies": { + "@graphql-tools/delegate": "^10.0.4", + "@graphql-tools/schema": "^10.0.3", + "@graphql-tools/utils": "^10.1.1", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kamilkisiela/fast-url-parser": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", + "integrity": "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==" + }, + "node_modules/@ljharb/through": { + "version": "2.3.12", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/fast-element": { + "version": "1.12.0", + "license": "MIT" + }, + "node_modules/@microsoft/fast-foundation": { + "version": "2.49.5", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-web-utilities": "^5.4.1", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + } + }, + "node_modules/@microsoft/fast-foundation/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@microsoft/fast-react-wrapper": { + "version": "0.3.23", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-foundation": "^2.49.5" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "5.4.1", + "license": "MIT", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.5.1", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@preact/signals-react": { + "version": "1.3.6", + "license": "MIT", + "dependencies": { + "@preact/signals-core": "^1.4.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.1", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" + } + }, + "node_modules/@repeaterjs/repeater": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.5.tgz", + "integrity": "sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@teamsupercell/typings-for-css-modules-loader": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "loader-utils": "^1.4.2", + "schema-utils": "^2.0.1" + }, + "optionalDependencies": { + "prettier": "*" + } + }, + "node_modules/@testim/chrome-version": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "8.56.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.18.79", + "license": "MIT" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.2.53", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.18", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.86.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/which": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/snapshot": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vscode/codicons": { + "version": "0.0.30", + "license": "CC-BY-4.0" + }, + "node_modules/@vscode/test-electron": { + "version": "2.3.9", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/vsce": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.25.0.tgz", + "integrity": "sha512-VXMCGUaP6wKBadA7vFQdsksxkBAMoh4ecZgXBwauZMASAgnwYesHyLnqIyWYeRwjy2uEpitHvz/1w5ENnR30pg==", + "dependencies": { + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 16" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vscode/vsce/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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vscode/vsce/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@vscode/vsce/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@vscode/webview-ui-toolkit": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-foundation": "^2.49.4", + "@microsoft/fast-react-wrapper": "^0.3.22", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.23.tgz", + "integrity": "sha512-HAFmuVEwNqNdmk+w4VCQ2pkLk1Vw4XYiiyxEp3z/xvl14aLTUBw2OfVH3vBcx+FtGsynQLkkhK410Nah1N2yyQ==", + "dependencies": { + "@babel/parser": "^7.24.1", + "@vue/shared": "3.4.23", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-core/node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.23.tgz", + "integrity": "sha512-t0b9WSTnCRrzsBGrDd1LNR5HGzYTr7LX3z6nNBG+KGvZLqrT0mY6NsMzOqlVMBKKXKVuusbbB5aOOFgTY+senw==", + "dependencies": { + "@vue/compiler-core": "3.4.23", + "@vue/shared": "3.4.23" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.23.tgz", + "integrity": "sha512-fSDTKTfzaRX1kNAUiaj8JB4AokikzStWgHooMhaxyjZerw624L+IAP/fvI4ZwMpwIh8f08PVzEnu4rg8/Npssw==", + "dependencies": { + "@babel/parser": "^7.24.1", + "@vue/compiler-core": "3.4.23", + "@vue/compiler-dom": "3.4.23", + "@vue/compiler-ssr": "3.4.23", + "@vue/shared": "3.4.23", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.8", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.23.tgz", + "integrity": "sha512-hb6Uj2cYs+tfqz71Wj6h3E5t6OKvb4MVcM2Nl5i/z1nv1gjEhw+zYaNOV+Xwn+SSN/VZM0DgANw5TuJfxfezPg==", + "dependencies": { + "@vue/compiler-dom": "3.4.23", + "@vue/shared": "3.4.23" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.23", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.23.tgz", + "integrity": "sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg==" + }, + "node_modules/@wdio/cli": { + "version": "8.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.1", + "@vitest/snapshot": "^1.2.1", + "@wdio/config": "8.29.3", + "@wdio/globals": "8.29.7", + "@wdio/logger": "8.28.0", + "@wdio/protocols": "8.29.7", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.3", + "async-exit-hook": "^2.0.1", + "chalk": "^5.2.0", + "chokidar": "^3.5.3", + "cli-spinners": "^2.9.0", + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "execa": "^8.0.1", + "import-meta-resolve": "^4.0.0", + "inquirer": "9.2.12", + "lodash.flattendeep": "^4.4.0", + "lodash.pickby": "^4.6.0", + "lodash.union": "^4.6.0", + "read-pkg-up": "^10.0.0", + "recursive-readdir": "^2.2.3", + "webdriverio": "8.29.7", + "yargs": "^17.7.2" + }, + "bin": { + "wdio": "bin/wdio.js" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/cli/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@wdio/config": { + "version": "8.29.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "8.28.0", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.3", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.0.0", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/config/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@wdio/config/node_modules/glob": { + "version": "10.3.10", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/config/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/globals": { + "version": "8.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.13 || >=18" + }, + "optionalDependencies": { + "expect-webdriverio": "^4.9.3", + "webdriverio": "8.29.7" + } + }, + "node_modules/@wdio/local-runner": { + "version": "8.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/logger": "8.28.0", + "@wdio/repl": "8.24.12", + "@wdio/runner": "8.29.7", + "@wdio/types": "8.29.1", + "async-exit-hook": "^2.0.1", + "split2": "^4.1.0", + "stream-buffers": "^3.0.2" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/local-runner/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@wdio/logger": { + "version": "8.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/mocha-framework": { + "version": "8.29.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.0", + "@types/node": "^20.1.0", + "@wdio/logger": "8.28.0", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.3", + "mocha": "^10.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/mocha-framework/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@wdio/protocols": { + "version": "8.29.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/repl": { + "version": "8.24.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@wdio/reporter": { + "version": "8.29.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/logger": "8.28.0", + "@wdio/types": "8.29.1", + "diff": "^5.0.0", + "object-inspect": "^1.12.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/reporter/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@wdio/runner": { + "version": "8.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/config": "8.29.3", + "@wdio/globals": "8.29.7", + "@wdio/logger": "8.28.0", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.3", + "deepmerge-ts": "^5.0.0", + "expect-webdriverio": "^4.9.3", + "gaze": "^1.1.2", + "webdriver": "8.29.7", + "webdriverio": "8.29.7" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@wdio/spec-reporter": { + "version": "8.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/reporter": "8.29.1", + "@wdio/types": "8.29.1", + "chalk": "^5.1.2", + "easy-table": "^1.2.0", + "pretty-ms": "^7.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/types": { + "version": "8.29.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/types/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@wdio/utils": { + "version": "8.29.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.28.0", + "@wdio/types": "8.29.1", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.3.5", + "geckodriver": "^4.3.1", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.1.0", + "safaridriver": "^0.1.0", + "split2": "^4.2.0", + "wait-port": "^1.0.4" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", + "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", + "dependencies": { + "@whatwg-node/node-fetch": "^0.5.7", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", + "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", + "dependencies": { + "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/events": "^0.1.0", + "busboy": "^1.6.0", + "fast-querystring": "^1.1.1", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.11.3", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "dev": true, + "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/archive-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/archive-type/node_modules/file-type": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/archiver": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^4.0.1", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^5.0.1" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/archiver-utils": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^8.0.0", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.1.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.5", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/avvio": { + "version": "8.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.6.1" + } + }, + "node_modules/axios": { + "version": "1.6.7", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/b4a": { + "version": "1.6.4", + "dev": true, + "license": "ISC" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "devOptional": true, + "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/basic-ftp": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.22.3", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "devOptional": true, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/cacheable-request/node_modules/json-buffer": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001584", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "dev": true, + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "optional": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromedriver": { + "version": "123.0.3", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.3.tgz", + "integrity": "sha512-35IeTqDLcVR0htF9nD/Lh+g24EG088WHVKXBXiFyWq+2lelnoM0B3tKTBiUEjLng0GnELI4QyQPFK7i97Fz1fQ==", + "dev": true, + "hasInstallScript": true, + "peer": true, + "dependencies": { + "@testim/chrome-version": "^1.1.4", + "axios": "^1.6.7", + "compare-versions": "^6.1.0", + "extract-zip": "^2.0.1", + "proxy-agent": "^6.4.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.2" + }, + "bin": { + "chromedriver": "bin/chromedriver" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chromedriver/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/chromedriver/node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/chromedriver/node_modules/https-proxy-agent": { + "version": "7.0.4", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/chromedriver/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/chromedriver/node_modules/proxy-agent": { + "version": "6.4.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/chromium-bidi": { + "version": "0.4.16", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/clipboardy/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/clipboardy/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clipboardy/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clipboardy/node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/clipboardy/node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "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/clone": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/cockatiel": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.2.tgz", + "integrity": "sha512-5yARKww0dWyWg2/3xZeXgoxjHLwpVqFptj9Zy7qioJ6+/L0ARM184sgMUrQDjxw7ePJWlGhV998mKhzrxT0/Kg==", + "engines": { + "node": ">=16" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/code-red/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compare-versions": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/compress-commons": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^5.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/cookie": { + "version": "0.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig-toml-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-toml-loader/-/cosmiconfig-toml-loader-1.0.0.tgz", + "integrity": "sha512-H/2gurFWVi7xXvCyvsWRLCMekl4tITJcX0QEsDMpzxtuxDyM59xLatYNg4s/k9AA/HdtCYfj2su8mgA0GSDLDA==", + "dependencies": { + "@iarna/toml": "^2.2.5" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "dev": true, + "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/cross-inspect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.0.tgz", + "integrity": "sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-shorthand-properties": { + "version": "1.1.1", + "dev": true + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-value": { + "version": "0.0.1", + "dev": true + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dataloader": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", + "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==" + }, + "node_modules/debug": { + "version": "4.3.4", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/bl": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/decompress-tar/node_modules/file-type": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-tar/node_modules/tar-stream": { + "version": "1.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/file-type": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", + "integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "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/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1249869", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.1", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/download": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "archive-type": "^4.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.2.1", + "ext-name": "^5.0.0", + "file-type": "^11.1.0", + "filenamify": "^3.0.0", + "get-stream": "^4.1.0", + "got": "^8.3.1", + "make-dir": "^2.1.0", + "p-event": "^2.1.0", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/download/node_modules/get-stream": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dset": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", + "integrity": "sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/easy-table": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "optionalDependencies": { + "wcwidth": "^1.0.1" + } + }, + "node_modules/edge-paths": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/shirshak55" + } + }, + "node_modules/edge-paths/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/edge-paths/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/edgedriver": { + "version": "5.3.9", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^8.16.17", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "node-fetch": "^3.3.2", + "unzipper": "^0.10.14", + "which": "^4.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.656", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.11.1", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "dev": true, + "license": "MIT", + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.33.2", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect-webdriverio": { + "version": "4.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/snapshot": "^1.2.2", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">=16 || >=18 || >=20" + }, + "optionalDependencies": { + "@wdio/globals": "^8.29.3", + "@wdio/logger": "^8.28.0", + "webdriverio": "^8.29.3" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "license": "Apache-2.0" + }, + "node_modules/ext-list": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extract-files": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", + "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==", + "engines": { + "node": "^12.20 || >= 14.13" + }, + "funding": { + "url": "https://github.com/sponsors/jaydenseric" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/deepmerge": "^1.0.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastify": { + "version": "4.26.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.2.1", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.0", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0", + "is-unicode-supported": "^1.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "vue-template-compiler": "*", + "webpack": "^5.11.0" + }, + "peerDependenciesMeta": { + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.5", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/geckodriver": { + "version": "4.3.2", + "dev": true, + "hasInstallScript": true, + "license": "MPL-2.0", + "dependencies": { + "@wdio/logger": "^8.28.0", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.4", + "unzipper": "^0.10.14", + "which": "^4.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": "^16.13 || >=18 || >=20" + } + }, + "node_modules/geckodriver/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/geckodriver/node_modules/http-proxy-agent": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/geckodriver/node_modules/https-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.0", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/get-uri/node_modules/jsonfile": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/get-uri/node_modules/universalify": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "optional": true + }, + "node_modules/glob": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globule": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/globule/node_modules/glob": { + "version": "7.1.7", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globule/node_modules/minimatch": { + "version": "3.0.8", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/got/node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/graphql": { + "version": "16.8.1", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-config": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-5.0.3.tgz", + "integrity": "sha512-BNGZaoxIBkv9yy6Y7omvsaBUHOzfFcII3UN++tpH8MGOKFPFkCPZuwx09ggANMt8FgyWP1Od8SWPmrUEZca4NQ==", + "dependencies": { + "@graphql-tools/graphql-file-loader": "^8.0.0", + "@graphql-tools/json-file-loader": "^8.0.0", + "@graphql-tools/load": "^8.0.0", + "@graphql-tools/merge": "^9.0.0", + "@graphql-tools/url-loader": "^8.0.0", + "@graphql-tools/utils": "^10.0.0", + "cosmiconfig": "^8.1.0", + "jiti": "^1.18.2", + "minimatch": "^4.2.3", + "string-env-interpolation": "^1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "cosmiconfig-toml-loader": "^1.0.0", + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "cosmiconfig-toml-loader": { + "optional": true + } + } + }, + "node_modules/graphql-config/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/graphql-config/node_modules/minimatch": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.3.tgz", + "integrity": "sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graphql-language-service": { + "version": "5.2.0", + "resolved": "file:graphql-language-service-5.2.0.tgz", + "integrity": "sha512-6QhpKCzy6OJ87X2eD792R8nPUdhM0UHSkwt30tWGG0/snXfZHlK/h6OTgivg4cv+nFTx26zP/QjRhuUepxeZEg==", + "license": "MIT", + "dependencies": { + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/graphql-language-service-server": { + "version": "2.12.0", + "resolved": "file:graphql-language-service-server-2.12.0.tgz", + "integrity": "sha512-+co6wwS8JTWM3qcRWtE/frkz+Yqy8TYta86nt+zjyBaFsJGePG1GRXBaTDiyyfvLJlKc/atFSinT0VCVeLYaIg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.5", + "@graphql-tools/code-file-loader": "8.0.3", + "@vue/compiler-sfc": "^3.4.5", + "cosmiconfig-toml-loader": "^1.0.0", + "dotenv": "10.0.0", + "fast-glob": "^3.2.7", + "glob": "^7.2.0", + "graphql-config": "5.0.3", + "graphql-language-service": "file:graphql-language-service-5.2.0.tgz", + "mkdirp": "^1.0.4", + "node-abort-controller": "^3.0.1", + "node-fetch": "3.3.2", + "nullthrows": "^1.0.0", + "source-map-js": "1.0.2", + "svelte": "^4.1.1", + "svelte2tsx": "^0.7.0", + "typescript": "^5.3.3", + "vscode-jsonrpc": "^8.0.1", + "vscode-languageserver": "^8.0.1", + "vscode-languageserver-types": "^3.17.2", + "vscode-uri": "^3.0.2" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + } + }, + "node_modules/graphql-language-service-server/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, + "node_modules/graphql-language-service-server/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graphql-language-service-server/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/graphql-ws": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz", + "integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbol-support-x": { + "version": "1.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-to-string-tag-x": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbol-support-x": "^1.4.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.2.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/hpagent": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "3.8.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "dev": true, + "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-proxy-agent": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "devOptional": true, + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.1", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/immutable": { + "version": "4.3.5", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "optional": true + }, + "node_modules/inquirer": { + "version": "9.2.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.11", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^5.0.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/into-stream": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ip": { + "version": "1.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-object": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is2": { + "version": "2.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "3.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isurl": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.8.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ky": { + "version": "0.33.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/light-my-request": { + "version": "5.11.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.5.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "2.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "1.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/locate-app": { + "version": "2.2.16", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "n12": "1.8.19", + "type-fest": "2.13.0", + "userhome": "1.0.0" + } + }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "2.13.0", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.pickby": { + "version": "4.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loglevel": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "node_modules/memfs": { + "version": "3.5.3", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meros": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.0.tgz", + "integrity": "sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "devOptional": true, + "license": "MIT" + }, + "node_modules/mnemonist": { + "version": "0.39.6", + "dev": true, + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "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/mocha/node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/n12": { + "version": "1.8.19", + "dev": true, + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-abi": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.58.0.tgz", + "integrity": "sha512-pXY1jnGf5T7b8UNzWzIqf0EkX4bx/w8N2AvwlGnk2SYYA/kzDVPaH0Dh0UG4EwxBB5eKOIZKPr8VAHSHL1DPGg==", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "optional": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-loader": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/node-loader/node_modules/loader-utils": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "6.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obliterator": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-event": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "ip": "^1.1.8", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/periscopic/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pino": { + "version": "8.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/buffer": { + "version": "6.0.3", + "dev": true, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "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/pino-abstract-transport/node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/cosmiconfig": { + "version": "8.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.15", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/postcss/node_modules/source-map-js": { + "version": "1.2.0", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/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==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "20.9.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.4.6", + "chromium-bidi": "0.4.16", + "cross-fetch": "4.0.0", + "debug": "4.3.4", + "devtools-protocol": "0.0.1147663", + "ws": "8.13.0" + }, + "engines": { + "node": ">=16.3.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "1.4.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.0", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1147663", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer-core/node_modules/http-proxy-agent": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/puppeteer-core/node_modules/https-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/puppeteer-core/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/puppeteer-core/node_modules/proxy-agent": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.13.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/yargs": { + "version": "17.7.1", + "dev": true, + "license": "MIT", + "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/puppeteer-core/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/qs": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/query-string": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "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/queue-tick": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^8.1.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "4.10.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/yocto-queue": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg/node_modules/lines-and-columns": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { + "version": "3.13.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.10.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rechoir/node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.0.0", + "get-intrinsic": "^1.2.3", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/responselike": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/resq": { + "version": "1.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/ret": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safaridriver": { + "version": "0.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-array-concat": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.70.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.4", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "license": "ISC" + }, + "node_modules/serialize-error": { + "version": "11.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^2.12.2" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "2.19.0", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "dev": true, + "license": "MIT" + }, + "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==", + "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/set-function-name": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sort-keys": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length/node_modules/sort-keys": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.4.0", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split2": { + "version": "4.2.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-buffers": { + "version": "3.0.2", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.15.7", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-env-interpolation": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", + "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==" + }, + "node_modules/string-replace-loader": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "peerDependencies": { + "webpack": "^5" + } + }, + "node_modules/string-replace-loader/node_modules/loader-utils": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/string-replace-loader/node_modules/schema-utils": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.14.tgz", + "integrity": "sha512-ry3+YlWqZpHxLy45MW4MZIxNdvB+Wl7p2nnstWKbOAewaJyNJuOtivSbRChcfIej6wFBjWqyKmf/NgK1uW2JAA==", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/svelte2tsx": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.6.tgz", + "integrity": "sha512-awHvYsakyiGjRqqSOhb2F+qJ6lUT9klQe0UQofAcdHNaKKeDHA8kEZ8zYKGG3BiDPurKYMGvH5/lZ+jeIoG7yQ==", + "dependencies": { + "dedent-js": "^1.0.1", + "pascal-case": "^3.1.1" + }, + "peerDependencies": { + "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", + "typescript": "^4.9.4 || ^5.0.0" + } + }, + "node_modules/tabbable": { + "version": "5.3.3", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tcp-port-used": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/terser": { + "version": "5.27.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/through": { + "version": "2.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/timed-out": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmp-promise/node_modules/tmp": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-buffer": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/traverse": { + "version": "0.3.9", + "dev": true, + "license": "MIT/X11" + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/undici": { + "version": "5.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unixify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", + "integrity": "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==", + "dependencies": { + "normalize-path": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unixify/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/url-to-options": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/userhome": { + "version": "1.0.0", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" + }, + "engines": { + "vscode": "^1.67.0" + } + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", + "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", + "dependencies": { + "vscode-languageserver-protocol": "3.17.3" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.3", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "license": "MIT" + }, + "node_modules/wait-port": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wait-port/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wait-port/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/wait-port/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wait-port/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/wait-port/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/wdio-chromedriver-service": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^8.1.0", + "fs-extra": "^11.1.0", + "split2": "^4.1.0", + "tcp-port-used": "^1.0.2" + }, + "engines": { + "node": "^16.13 || >=18" + }, + "peerDependencies": { + "@wdio/types": "^7.0.0 || ^8.0.0-alpha.219", + "chromedriver": "*", + "webdriverio": "^7.0.0 || ^8.0.0-alpha.219" + }, + "peerDependenciesMeta": { + "@wdio/types": { + "optional": true + }, + "chromedriver": { + "optional": true + }, + "webdriverio": { + "optional": false + } + } + }, + "node_modules/wdio-chromedriver-service/node_modules/fs-extra": { + "version": "11.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/wdio-vscode-service": { + "version": "5.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/cors": "^8.3.0", + "@fastify/static": "^6.10.2", + "@types/ws": "^8.5.5", + "@vscode/test-electron": "^2.3.4", + "@wdio/logger": "^8.11.0", + "clipboardy": "^3.0.0", + "decamelize": "6.0.0", + "download": "^8.0.0", + "fastify": "^4.21.0", + "get-port": "7.0.0", + "hpagent": "^1.2.0", + "slash": "^5.1.0", + "tmp-promise": "^3.0.3", + "undici": "^5.23.0", + "vscode-uri": "^3.0.8", + "wdio-chromedriver-service": "^8.1.1", + "ws": "^8.13.0", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": "^16.13 || >=18" + }, + "peerDependencies": { + "chromedriver": "latest", + "webdriverio": "^8.0.0" + }, + "peerDependenciesMeta": { + "chromedriver": { + "optional": false + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/wdio-vscode-service/node_modules/slash": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wdio-vscode-service/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.2", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webdriver": { + "version": "8.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "8.29.3", + "@wdio/logger": "8.28.0", + "@wdio/protocols": "8.29.7", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.3", + "deepmerge-ts": "^5.1.0", + "got": "^12.6.1", + "ky": "^0.33.0", + "ws": "^8.8.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriver/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/webdriver/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/webdriver/node_modules/cacheable-request": { + "version": "10.2.14", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/webdriver/node_modules/decompress-response": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriver/node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriver/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriver/node_modules/got": { + "version": "12.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/webdriver/node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/webdriver/node_modules/lowercase-keys": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriver/node_modules/mimic-response": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriver/node_modules/normalize-url": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriver/node_modules/p-cancelable": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/webdriver/node_modules/responselike": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriverio": { + "version": "8.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/config": "8.29.3", + "@wdio/logger": "8.28.0", + "@wdio/protocols": "8.29.7", + "@wdio/repl": "8.24.12", + "@wdio/types": "8.29.1", + "@wdio/utils": "8.29.3", + "archiver": "^6.0.0", + "aria-query": "^5.0.0", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "devtools-protocol": "^0.0.1249869", + "grapheme-splitter": "^1.0.2", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.0", + "puppeteer-core": "^20.9.0", + "query-selector-shadow-dom": "^1.0.0", + "resq": "^1.9.1", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.1", + "webdriver": "8.29.7" + }, + "engines": { + "node": "^16.13 || >=18" + }, + "peerDependencies": { + "devtools": "^8.14.0" + }, + "peerDependenciesMeta": { + "devtools": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@types/node": { + "version": "20.11.16", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/webdriverio/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/webdriverio/node_modules/is-plain-obj": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriverio/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.90.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.14", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/workerpool": { + "version": "6.2.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "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/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.16.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "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": "20.2.4", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^4.0.1", + "compress-commons": "^5.0.1", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/firebase-vscode/package.json b/firebase-vscode/package.json new file mode 100644 index 00000000000..2a29a8d71a2 --- /dev/null +++ b/firebase-vscode/package.json @@ -0,0 +1,256 @@ +{ + "name": "firebase-vscode", + "displayName": "Firebase", + "publisher": "firebase", + "icon": "./resources/firebase_logo.png", + "description": "VSCode Extension for Firebase", + "version": "0.3.0", + "engines": { + "vscode": "^1.69.0" + }, + "repository": "https://github.com/firebase/firebase-tools", + "sideEffects": false, + "categories": [ + "Other" + ], + "extensionDependencies": [ + "graphql.vscode-graphql-syntax" + ], + "activationEvents": [ + "onStartupFinished", + "onLanguage:graphql", + "workspaceContains:**/.graphqlrc", + "workspaceContains:**/.graphqlrc.{json,yaml,yml,js,ts,toml}", + "workspaceContains:**/graphql.config.{json,yaml,yml,js,ts,toml}" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "fdc-graphql.showOutputChannel", + "title": "Firebase GraphQL Language Server: show output channel" + }, + { + "command": "fdc-graphql.restart", + "title": "Firebase GraphQL Language Server: Restart" + }, + { + "command": "fdc.deploy", + "title": "Firebase Data Connect: Deploy", + "icon": "$(cloud-upload)" + }, + { + "command": "firebase.selectProject", + "title": "Firebase: Switch project" + } + ], + "configuration": { + "title": "Firebase VS Code Extension", + "properties": { + "firebase.debug": { + "type": "boolean", + "default": true, + "description": "Enable writing debug-level messages to the file provided in firebase.debugLogPath (requires restart)" + }, + "firebase.debugLogPath": { + "type": "string", + "default": "/tmp/firebase-plugin.log", + "description": "If firebase.debug is true, appends debug-level messages to the provided file (requires restart)" + }, + "firebase.npmPath": { + "type": "string", + "default": "", + "description": "Path to NPM executable in local environment" + }, + "firebase.useFrameworks": { + "type": "boolean", + "default": true, + "description": "Enable web frameworks" + }, + "firebase.dataConnect.alwaysAllowMutationsInProduction": { + "type": "boolean", + "default": false, + "description": "Always allow mutations in production. If false (default), trying to run a mutation in production will open a confirmation modal." + } + } + }, + "keybindings": [ + { + "command": "firebase.dataConnect.executeOperationAtCursor", + "key": "ctrl+enter", + "mac": "cmd+enter", + "when": "editorLangId == gql || editorLangId == graphql" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "firebase", + "title": "Firebase", + "icon": "$(mono-firebase)" + } + ], + "panel": [ + { + "id": "firebase-data-connect-execution-view", + "title": "Data Connect Execution", + "icon": "$(mono-firebase)" + } + ] + }, + "icons": { + "mono-firebase": { + "description": "Firebase icon", + "default": { + "fontPath": "./resources/monicons.woff", + "fontCharacter": "\\F101" + } + }, + "data-connect": { + "description": "Data Connect icon", + "default": { + "fontPath": "./resources/GMPIcons.woff2", + "fontCharacter": "\\gmp_nav20_dataconnect" + } + } + }, + "views": { + "firebase": [ + { + "type": "webview", + "id": "sidebar", + "name": "Config" + }, + { + "type": "webview", + "id": "data-connect", + "name": "Firebase Data Connect", + "when": "firebase-vscode.fdc.enabled" + }, + { + "id": "firebase.dataConnect.explorerView", + "name": "FDC Explorer", + "when": "firebase-vscode.fdc.enabled" + } + ], + "firebase-data-connect-execution-view": [ + { + "type": "webview", + "id": "data-connect-execution-configuration", + "name": "Configuration", + "when": "firebase-vscode.fdc.enabled" + }, + { + "id": "data-connect-execution-history", + "name": "History", + "when": "firebase-vscode.fdc.enabled" + }, + { + "type": "webview", + "id": "data-connect-execution-results", + "name": "Results", + "when": "firebase-vscode.fdc.enabled" + } + ] + }, + "viewsWelcome": [ + { + "view": "firebase.dataConnect.explorerView", + "contents": "Start the emulator in a Data Connect project to view your schema." + } + ], + "jsonValidation": [ + { + "fileMatch": "firebase.json", + "url": "https://raw.githubusercontent.com/firebase/firebase-tools/master/schema/firebase-config.json" + } + ], + "yamlValidation": [ + { + "fileMatch": "extension.yaml", + "url": "https://raw.githubusercontent.com/firebase/firebase-tools/master/schema/extension-yaml.json" + }, + { + "fileMatch": "dataconnect.yaml", + "url": "./dist/schema/dataconnect-yaml.json" + }, + { + "fileMatch": "connector.yaml", + "url": "./dist/schema/connector-yaml.json" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run build", + "copyfiles": "cp -r node_modules/@vscode/codicons/dist resources/dist", + "pkg": "vsce package", + "dev": "npm run copyfiles && webpack --config webpack.dev.js", + "dev:extension": "npm run copyfiles && webpack --config webpack.dev.js --config-name extension", + "dev:sidebar": "npm run copyfiles && webpack --config webpack.dev.js --config-name sidebar", + "watch": "npm run copyfiles && webpack --config webpack.dev.js --watch", + "build": "npm run copyfiles && webpack --config webpack.prod.js --devtool hidden-source-map", + "build:extension": "webpack --config webpack.prod.js --config-name extension", + "build:sidebar": "npm run copyfiles && webpack --config webpack.prod.js --config-name sidebar", + "test-compile": "npm run copyfiles && webpack --config src/test/webpack.test.js", + "lint": "eslint src --ext ts", + "test": "npm run test:unit && npm run test:e2e", + "pretest:unit": "npm run test-compile && npm run build && tsc -p src/test/tsconfig.test.json", + "test:unit": "node ./dist/test/firebase-vscode/src/test/runTest.js", + "test:e2e": "npm run test:e2e:empty && npm run test:e2e:fishfood", + "test:e2e:empty": "TS_NODE_PROJECT=\"./src/test/tsconfig.test.json\" TEST=true wdio run ./src/test/empty_wdio.conf.ts", + "test:e2e:fishfood": "TS_NODE_PROJECT=\"./src/test/tsconfig.test.json\" TEST=true wdio run ./src/test/fishfood_wdio.conf.ts", + "format": "npx prettier . -w" + }, + "dependencies": { + "@preact/signals-core": "^1.4.0", + "@preact/signals-react": "1.3.6", + "@vscode/codicons": "0.0.30", + "@vscode/vsce": "^2.25.0", + "@vscode/webview-ui-toolkit": "^1.2.1", + "classnames": "^2.3.2", + "exponential-backoff": "3.1.1", + "graphql-language-service": "file:graphql-language-service-5.2.0.tgz", + "graphql-language-service-server": "file:graphql-language-service-server-2.12.0.tgz", + "js-yaml": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vscode-languageclient": "8.1.0" + }, + "devDependencies": { + "@teamsupercell/typings-for-css-modules-loader": "^2.5.1", + "@types/glob": "^8.0.0", + "@types/mocha": "^10.0.1", + "@types/node": "16.x", + "@types/react": "^18.0.9", + "@types/react-dom": "^18.0.4", + "@types/vscode": "^1.69.0", + "@typescript-eslint/eslint-plugin": "^5.45.0", + "@typescript-eslint/parser": "^5.45.0", + "@vscode/test-electron": "^2.2.0", + "@wdio/cli": "^8.27.1", + "@wdio/local-runner": "^8.27.0", + "@wdio/mocha-framework": "^8.27.0", + "@wdio/spec-reporter": "^8.27.0", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.7.1", + "eslint": "^8.28.0", + "eslint-plugin-react": "^7.32.2", + "fork-ts-checker-webpack-plugin": "^7.3.0", + "glob": "^8.0.3", + "graphql": "^16.7.1", + "mini-css-extract-plugin": "^2.6.0", + "mocha": "^10.1.0", + "node-loader": "2.0.0", + "postcss-loader": "^7.0.0", + "prettier": "^3.1.1", + "sass": "^1.52.0", + "sass-loader": "^13.0.0", + "string-replace-loader": "^3.1.0", + "ts-loader": "^9.4.2", + "typescript": "^4.9.3", + "wdio-vscode-service": "^5.2.2", + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1", + "webpack-merge": "^5.8.0" + } +} diff --git a/firebase-vscode/resources/GMPIcons.woff2 b/firebase-vscode/resources/GMPIcons.woff2 new file mode 100644 index 00000000000..55a7fb94f48 Binary files /dev/null and b/firebase-vscode/resources/GMPIcons.woff2 differ diff --git a/firebase-vscode/resources/firebase_logo.png b/firebase-vscode/resources/firebase_logo.png new file mode 100644 index 00000000000..c5e09f683f3 Binary files /dev/null and b/firebase-vscode/resources/firebase_logo.png differ diff --git a/firebase-vscode/resources/monicons.woff b/firebase-vscode/resources/monicons.woff new file mode 100644 index 00000000000..815694ade6b Binary files /dev/null and b/firebase-vscode/resources/monicons.woff differ diff --git a/firebase-vscode/src/analytics.ts b/firebase-vscode/src/analytics.ts new file mode 100644 index 00000000000..28e57482a5d --- /dev/null +++ b/firebase-vscode/src/analytics.ts @@ -0,0 +1,60 @@ +import { env, TelemetryLogger, TelemetrySender } from "vscode"; +import { pluginLogger } from "./logger-wrapper"; +import { AnalyticsParams, trackVSCode } from "../../src/track"; + +export enum DATA_CONNECT_EVENT_NAME { + COMMAND_EXECUTION = "command_execution", + DEPLOY_ALL = "deploy_all", + DEPLOY_INDIVIDUAL = "deploy_individual", + IDX_LOGIN = "idx_login", + LOGIN = "login", + PROJECT_SELECT = "project_select", + RUN_LOCAL = "run_local", + RUN_PROD = "run_prod", + ADD_DATA = "add_data", + READ_DATA = "read_data", + MOVE_TO_CONNECTOR = "move_to_connector", + START_EMULATOR_FROM_EXECUTION = "start_emulator_from_execution", + REFUSE_START_EMULATOR_FROM_EXECUTION = "refuse_start_emulator_from_execution", +} + +export class AnalyticsLogger { + readonly logger: TelemetryLogger; + constructor() { + this.logger = env.createTelemetryLogger( + new GA4TelemetrySender(pluginLogger), + ); + } +} + +class GA4TelemetrySender implements TelemetrySender { + constructor( + readonly pluginLogger, + ) {} + + sendEventData( + eventName: string, + data?: Record | undefined, + ): void { + if (!env.isTelemetryEnabled) { + this.pluginLogger.warn("Telemetry is not enabled."); + return; + } + + // telemetry logger adds prefixes to eventName and params that are disallowed in GA4 + eventName = eventName.replace("firebase.firebase-vscode/", ""); + for (const key in data) { + if (key.includes("common.")) { + data[key.replace("common.", "")] = data[key]; + delete data[key]; + } + } + data = { ...data }; + trackVSCode(eventName, data as AnalyticsParams); + } + + sendErrorData(error: Error, data?: Record | undefined): void { + // n/a + // TODO: Sanatize error messages for user data + } +} diff --git a/firebase-vscode/src/auth/service.ts b/firebase-vscode/src/auth/service.ts new file mode 100644 index 00000000000..7d1b6525608 --- /dev/null +++ b/firebase-vscode/src/auth/service.ts @@ -0,0 +1,23 @@ +import { Disposable } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { UserMock } from "../../common/messaging/protocol"; + +export class AuthService implements Disposable { + constructor(readonly broker: ExtensionBrokerImpl) { + this.disposable.push({ + dispose: broker.on( + "notifyAuthUserMockChange", + (userMock) => (this.userMock = userMock) + ), + }); + } + + userMock: UserMock | undefined; + disposable: Disposable[] = []; + + dispose() { + for (const disposable of this.disposable) { + disposable.dispose(); + } + } +} diff --git a/firebase-vscode/src/cli.ts b/firebase-vscode/src/cli.ts new file mode 100644 index 00000000000..2e56f53128e --- /dev/null +++ b/firebase-vscode/src/cli.ts @@ -0,0 +1,234 @@ +import * as vscode from "vscode"; +import { inspect } from "util"; + +import { + getAllAccounts, + getGlobalDefaultAccount, + loginGoogle, + setGlobalDefaultAccount, +} from "../../src/auth"; +import { logoutAction } from "../../src/commands/logout"; +import { listFirebaseProjects } from "../../src/management/projects"; +import { requireAuth } from "../../src/requireAuth"; +import { Account, User } from "../../src/types/auth"; +import { Options } from "../../src/options"; +import { currentOptions, getCommandOptions } from "./options"; +import { ServiceAccount } from "../common/types"; +import { EmulatorUiSelections } from "../common/messaging/types"; +import { pluginLogger } from "./logger-wrapper"; +import { setAccessToken } from "../../src/apiv2"; +import { + startAll as startAllEmulators, + cleanShutdown as stopAllEmulators, +} from "../../src/emulator/controller"; +import { EmulatorRegistry } from "../../src/emulator/registry"; +import { + DownloadableEmulatorDetails, + EmulatorInfo, + DownloadableEmulators, + Emulators, +} from "../../src/emulator/types"; +import * as commandUtils from "../../src/emulator/commandUtils"; +import { currentUser } from "./core/user"; +import { firstWhere } from "./utils/signal"; +export { Emulators }; +/** + * Try to get a service account by calling requireAuth() without + * providing any account info. + */ +async function getServiceAccount() { + let email = null; + try { + // Make sure no user/token is sent + // to requireAuth which would prevent autoAuth() from being reached. + // We do need to send isVSCE to prevent project selection popup + const optionsMinusUser = await getCommandOptions(undefined, { + ...currentOptions.value, + }); + delete optionsMinusUser.user; + delete optionsMinusUser.tokens; + delete optionsMinusUser.token; + email = (await requireAuth(optionsMinusUser)) || null; + } catch (e) { + let errorMessage = e.message; + if (e.original?.message) { + errorMessage += ` (original: ${e.original.message})`; + } + pluginLogger.debug( + `No service account found (this may be normal), ` + + `requireAuth error output: ${errorMessage}`, + ); + return null; + } + if (process.env.WORKSPACE_SERVICE_ACCOUNT_EMAIL) { + // If Monospace, get service account email using env variable as + // the metadata server doesn't currently return the credentials + // for the workspace service account. Remove when Monospace is + // updated to return credentials through the metadata server. + pluginLogger.debug( + `Using WORKSPACE_SERVICE_ACCOUNT_EMAIL env ` + + `variable to get service account email: ` + + `${process.env.WORKSPACE_SERVICE_ACCOUNT_EMAIL}`, + ); + return process.env.WORKSPACE_SERVICE_ACCOUNT_EMAIL; + } + pluginLogger.debug( + `Got service account email through credentials:` + ` ${email}`, + ); + return email; +} + +/** + * Wrap the CLI's requireAuth() which is normally run before every command + * requiring user to be logged in. The CLI automatically supplies it with + * account info if found in configstore so we need to fill that part in. + */ +async function requireAuthWrapper(showError: boolean = true): Promise { + // Try to get global default from configstore. For some reason this is + pluginLogger.debug("requireAuthWrapper"); + let account = getGlobalDefaultAccount(); + // often overwritten when restarting the extension. + if (!account) { + // If nothing in configstore top level, grab the first "additionalAccount" + const accounts = getAllAccounts(); + for (const additionalAccount of accounts) { + if (additionalAccount.user.email === currentUser.value.email) { + account = additionalAccount; + setGlobalDefaultAccount(account); + } + } + } + const commandOptions = await getCommandOptions(undefined, { + ...currentOptions.value, + }); + // `requireAuth()` is not just a check, but will also register SERVICE + // ACCOUNT tokens in memory as a variable in apiv2.ts, which is needed + // for subsequent API calls. Warning: this variable takes precedence + // over Google login tokens and must be removed if a Google + // account is the current user. + try { + const serviceAccountEmail = await getServiceAccount(); + // Priority 1: Service account exists and is the current selected user + if ( + serviceAccountEmail && + currentUser.value.email === serviceAccountEmail + ) { + // requireAuth should have been run and apiv2 token should be stored + // already due to getServiceAccount() call above. + return true; + } else if (account) { + // Priority 2: Google login account exists and is the currently selected + // user + // Priority 3: Google login account exists and there is no selected user + // Clear service account access token from memory in apiv2. + setAccessToken(); + await requireAuth({ ...commandOptions, ...account }); + return true; + } else if (serviceAccountEmail) { + // Priority 4: There is a service account but it's not set as + // currentUser for some reason, but there also isn't an oauth account. + // requireAuth was already run as part of getServiceAccount() above + return true; + } + pluginLogger.debug("No user found (this may be normal)"); + return false; + } catch (e) { + if (showError) { + // Show error to user - show a popup and log it with log level "error". + pluginLogger.error( + `requireAuth error: ${e.original?.message || e.message}`, + ); + vscode.window.showErrorMessage("Not logged in", { + modal: true, + detail: `Log in by clicking "Sign in with Google" in the sidebar.`, + }); + } else { + // User shouldn't need to see this error - not actionable, + // but we should log it for debugging purposes. + pluginLogger.debug( + "requireAuth error output: ", + e.original?.message || e.message, + ); + } + return false; + } +} + +export async function getAccounts(): Promise> { + // Get Firebase login accounts + const accounts: Array = getAllAccounts(); + pluginLogger.debug(`Found ${accounts.length} non-service accounts.`); + // Get other accounts (assuming service account for now, could also be glogin) + const serviceAccountEmail = await getServiceAccount(); + if (serviceAccountEmail) { + pluginLogger.debug(`Found service account: ${serviceAccountEmail}`); + accounts.push({ + user: { email: serviceAccountEmail, type: "service_account" }, + }); + } + return accounts; +} + +export async function logoutUser(email: string): Promise { + await logoutAction(email, {} as Options); +} + +/** + * Login with standard Firebase login + */ +export async function login() { + const userCredentials = await loginGoogle(true); + setGlobalDefaultAccount(userCredentials as Account); + return userCredentials as { user: User }; +} + +export async function listProjects() { + const loggedIn = await requireAuthWrapper(false); + if (!loggedIn) { + return []; + } + return listFirebaseProjects(); +} + +export async function emulatorsStart( + emulatorUiSelections: EmulatorUiSelections, +) { + const only = emulatorUiSelections.mode === "dataconnect" + ? `${Emulators.DATACONNECT},${Emulators.AUTH}` + : ""; + const commandOptions = await getCommandOptions(undefined, { + ...(await firstWhere( + // TODO use firstWhereDefined once currentOptions are undefined if not initialized yet + currentOptions, + (op) => !!op && op.configPath.length !== 0, + )), + project: emulatorUiSelections.projectId, + exportOnExit: emulatorUiSelections.exportStateOnExit, + import: emulatorUiSelections.importStateFolderPath, + only, + }); + // Adjusts some options, export on exit can be a boolean or a path. + commandUtils.setExportOnExitOptions( + commandOptions as commandUtils.ExportOnExitOptions, + ); + return startAllEmulators(commandOptions, /*showUi=*/ true); +} + +export async function stopEmulators() { + await stopAllEmulators(); +} + +export function listRunningEmulators(): EmulatorInfo[] { + return EmulatorRegistry.listRunningWithInfo(); +} + +export function getEmulatorUiUrl(): string | undefined { + const url: URL = EmulatorRegistry.url(Emulators.UI); + return url.hostname === "unknown" ? undefined : url.toString(); +} + +export function getEmulatorDetails( + emulator: DownloadableEmulators, +): DownloadableEmulatorDetails { + return EmulatorRegistry.getDetails(emulator); +} diff --git a/firebase-vscode/src/core/config.ts b/firebase-vscode/src/core/config.ts new file mode 100644 index 00000000000..1f9909d0b05 --- /dev/null +++ b/firebase-vscode/src/core/config.ts @@ -0,0 +1,277 @@ +import { Disposable, FileSystemWatcher } from "vscode"; +import * as vscode from "vscode"; +import path from "path"; +import fs from "fs"; +import { currentOptions } from "../options"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { RC, RCData } from "../../../src/rc"; +import { Config } from "../../../src/config"; +import { globalSignal } from "../utils/globals"; +import { workspace } from "../utils/test_hooks"; +import { ResolvedDataConnectConfigs } from "../data-connect/config"; +import { ValueOrError } from "../../common/messaging/protocol"; +import { firstWhereDefined, onChange } from "../utils/signal"; +import { Result, ResultError, ResultValue } from "../result"; +import { FirebaseConfig } from "../firebaseConfig"; +import { effect } from "@preact/signals-react"; + +/** + * The .firebaserc configs. + * + * `undefined` means that the extension has yet to load the file. + * {@link ResultValue} with an `undefined` value means that the file was not found. + * {@link ResultError} means that the file was found but the parsing failed. + * + * This enables the UI to differentiate between "no config" and "error reading config", + * and also await for configs to be loaded (thanks to the {@link firstWhereDefined} util) + */ +export const firebaseRC = globalSignal | undefined>( + undefined, +); + +export const dataConnectConfigs = globalSignal< + ResolvedDataConnectConfigs | undefined +>(undefined); + +/** + * The firebase.json configs. + * + * `undefined` means that the extension has yet to load the file. + * {@link ResultValue} with an `undefined` value means that the file was not found. + * {@link ResultError} means that the file was found but the parsing failed. + * + * This enables the UI to differentiate between "no config" and "error reading config", + * and also await for configs to be loaded (thanks to the {@link firstWhereDefined} util) + */ +export const firebaseConfig = globalSignal< + Result | undefined +>(undefined); + +/** + * Write new default project to .firebaserc + */ +export async function updateFirebaseRCProject(values: { + fdcPostgresConnectionString?: string; + projectAlias?: { + alias: string; + projectId: string; + }; +}) { + const rc = + firebaseRC.value.tryReadValue ?? + // We don't update firebaseRC if we create a temporary RC, + // as the file watcher will update the value for us. + // This is only for the sake of calling `save()`. + new RC(path.join(currentOptions.value.cwd, ".firebaserc"), {}); + + if (values.projectAlias) { + if ( + rc.resolveAlias(values.projectAlias.alias) === + values.projectAlias.projectId + ) { + // Nothing to update, avoid an unnecessary write. + // That's especially important as a write will trigger file watchers, + // which may then re-trigger this function. + return; + } + + rc.addProjectAlias( + values.projectAlias.alias, + values.projectAlias.projectId, + ); + } + + if (values.fdcPostgresConnectionString) { + rc.setDataconnect(values.fdcPostgresConnectionString); + } + + rc.save(); +} + +function notifyFirebaseConfig(broker: ExtensionBrokerImpl) { + broker.send("notifyFirebaseConfig", { + firebaseJson: firebaseConfig.value?.switchCase< + ValueOrError | undefined + >( + (value) => ({ value: value?.data, error: undefined }), + (error) => ({ value: undefined, error: `${error}` }), + ), + firebaseRC: firebaseRC.value?.switchCase< + ValueOrError | undefined + >( + (value) => ({ + value: value?.data, + error: undefined, + }), + (error) => ({ value: undefined, error: `${error}` }), + ), + }); +} + +function registerRc(broker: ExtensionBrokerImpl): Disposable { + firebaseRC.value = _readRC(); + const rcRemoveListener = onChange(firebaseRC, () => + notifyFirebaseConfig(broker), + ); + + const showToastOnError = effect(() => { + const rc = firebaseRC.value; + if (rc instanceof ResultError) { + vscode.window.showErrorMessage(`Error reading .firebaserc:\n${rc.error}`); + } + }); + + const rcWatcher = _createWatcher(".firebaserc"); + rcWatcher?.onDidChange(() => (firebaseRC.value = _readRC())); + rcWatcher?.onDidCreate(() => (firebaseRC.value = _readRC())); + // TODO handle deletion of .firebaserc/.firebase.json/firemat.yaml + rcWatcher?.onDidDelete(() => (firebaseRC.value = undefined)); + + return Disposable.from( + { dispose: rcRemoveListener }, + { dispose: showToastOnError }, + { dispose: () => rcWatcher?.dispose() }, + ); +} + +function registerFirebaseConfig(broker: ExtensionBrokerImpl): Disposable { + firebaseConfig.value = _readFirebaseConfig(); + + const firebaseConfigRemoveListener = onChange(firebaseConfig, () => + notifyFirebaseConfig(broker), + ); + + const showToastOnError = effect(() => { + const config = firebaseConfig.value; + if (config instanceof ResultError) { + vscode.window.showErrorMessage( + `Error reading firebase.json:\n${config.error}`, + ); + } + }); + + const configWatcher = _createWatcher("firebase.json"); + configWatcher?.onDidChange( + () => (firebaseConfig.value = _readFirebaseConfig()), + ); + configWatcher?.onDidCreate( + () => (firebaseConfig.value = _readFirebaseConfig()), + ); + configWatcher?.onDidDelete(() => (firebaseConfig.value = undefined)); + + return Disposable.from( + { dispose: firebaseConfigRemoveListener }, + { dispose: showToastOnError }, + { dispose: () => configWatcher?.dispose() }, + ); +} + +export function registerConfig(broker: ExtensionBrokerImpl): Disposable { + // On getInitialData, forcibly notifies the extension. + const getInitialDataRemoveListener = broker.on("getInitialData", () => { + notifyFirebaseConfig(broker); + }); + + // TODO handle deletion of .firebaserc/.firebase.json/firemat.yaml + + return Disposable.from( + { dispose: getInitialDataRemoveListener }, + registerFirebaseConfig(broker), + registerRc(broker), + ); +} + +/** @internal */ +export function _readRC(): Result { + return Result.guard(() => { + const configPath = getConfigPath(); + if (!configPath) { + return undefined; + } + // RC.loadFile silences errors and returns a non-empty object if the rc file is + // missing. Let's load it ourselves. + + const rcPath = path.join(configPath, ".firebaserc"); + + if (!fs.existsSync(rcPath)) { + return undefined; + } + + const json = fs.readFileSync(rcPath); + const data = JSON.parse(json.toString()); + + return new RC(rcPath, data); + }); +} + +/** @internal */ +export function _readFirebaseConfig(): Result { + const result = Result.guard(() => { + const configPath = getConfigPath(); + if (!configPath) { + return undefined; + } + const config = Config.load({ + configPath: path.join(configPath, "firebase.json"), + }); + if (!config) { + // Config.load may return null. We transform it to undefined. + return undefined; + } + + return config; + }); + + if (result instanceof ResultError && (result.error as any).status === 404) { + return undefined; + } + + return result; +} + +/** @internal */ +export function _createWatcher(file: string): FileSystemWatcher | undefined { + if (!currentOptions.value.cwd) { + return undefined; + } + + return workspace.value?.createFileSystemWatcher( + // Using RelativePattern enables tests to use watchers too. + new vscode.RelativePattern(vscode.Uri.file(currentOptions.value.cwd), file), + ); +} + +export function getRootFolders() { + const ws = workspace.value; + if (!ws) { + return []; + } + const folders = ws.workspaceFolders + ? ws.workspaceFolders.map((wf) => wf.uri.fsPath) + : []; + if (ws.workspaceFile) { + folders.push(path.dirname(ws.workspaceFile.fsPath)); + } + return Array.from(new Set(folders)); +} + +export function getConfigPath(): string | undefined { + // Usually there's only one root folder unless someone is using a + // multi-root VS Code workspace. + // https://code.visualstudio.com/docs/editor/multi-root-workspaces + // We are trying to play it safe by assigning the cwd + // based on where a .firebaserc or firebase.json was found but if + // the user hasn't run firebase init there won't be one, and without + // a cwd we won't know where to put it. + const rootFolders = getRootFolders(); + + let folder = rootFolders.find((folder) => { + return ( + fs.existsSync(path.join(folder, ".firebaserc")) || + fs.existsSync(path.join(folder, "firebase.json")) + ); + }); + + folder ??= rootFolders[0]; + return folder; +} diff --git a/firebase-vscode/src/core/emulators.ts b/firebase-vscode/src/core/emulators.ts new file mode 100644 index 00000000000..ddcb7a791a6 --- /dev/null +++ b/firebase-vscode/src/core/emulators.ts @@ -0,0 +1,240 @@ +import vscode, { Disposable, ThemeColor } from "vscode"; +import { + emulatorsStart, + listRunningEmulators, + stopEmulators, + Emulators, +} from "../cli"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { computed, effect, signal } from "@preact/signals-core"; +import { + DEFAULT_EMULATOR_UI_SELECTIONS, + ExtensionToWebviewParamsMap, +} from "../../common/messaging/protocol"; +import { firebaseRC } from "./config"; +import { EmulatorUiSelections } from "../messaging/types"; + +export class EmulatorsController implements Disposable { + constructor(private broker: ExtensionBrokerImpl) { + this.emulatorStatusItem.command = "firebase.openFirebaseRc"; + + this.subscriptions.push( + broker.on("getEmulatorUiSelections", () => + this.notifyUISelectionChangedListeners(), + ), + ); + // Notify the UI of the emulator selections changes + this.subscriptions.push( + effect(() => { + // Listen for changes. + this.uiSelections.value; + + // TODO(christhompson): Save UI selections in the current workspace. + // Requires context object. + this.notifyUISelectionChangedListeners(); + }), + ); + + this.subscriptions.push( + broker.on("getEmulatorInfos", () => this.notifyEmulatorStateChanged()), + ); + this.subscriptions.push( + effect(() => { + // Listen for changes. + this.emulators.value; + + this.notifyEmulatorStateChanged(); + }), + ); + + this.subscriptions.push( + broker.on("updateEmulatorUiSelections", (uiSelections) => { + this.uiSelections.value = { + ...this.uiSelections.peek(), + ...uiSelections, + }; + }), + ); + + this.subscriptions.push( + broker.on("selectEmulatorImportFolder", async () => { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: `Pick an import folder`, + title: `Pick an import folder`, + canSelectFiles: false, + canSelectFolders: true, + }; + const fileUri = await vscode.window.showOpenDialog(options); + // Update the UI of the selection + if (!fileUri || fileUri.length < 1) { + vscode.window.showErrorMessage("Invalid import folder selected."); + return; + } + broker.send("notifyEmulatorImportFolder", { + folder: fileUri[0].fsPath, + }); + }), + ); + + this.subscriptions.push( + effect(() => { + const projectId = firebaseRC.value?.tryReadValue?.projects?.default; + this.uiSelections.value = { + ...this.uiSelections.peek(), + projectId: this.getProjectIdForMode( + projectId, + this.uiSelections.peek().mode, + ), + }; + }), + ); + } + + readonly emulatorStatusItem = vscode.window.createStatusBarItem("emulators"); + + private pendingEmulatorStart: Promise | undefined; + + private readonly waitCommand = vscode.commands.registerCommand( + "firebase.emulators.wait", + this.waitEmulators.bind(this), + ); + + // TODO(christhompson): Load UI selections from the current workspace. + // Requires context object. + readonly uiSelections = signal(DEFAULT_EMULATOR_UI_SELECTIONS); + + readonly emulatorStates = computed(() => { + if (!this.areEmulatorsRunning.value) { + return undefined; + } + + // TODO(rrousselGit) handle cases where one emulator is running, + // and a new one is started. + return listRunningEmulators(); + }); + + readonly emulators = signal< + ExtensionToWebviewParamsMap["notifyEmulatorStateChanged"] + >({ + status: "stopped", + infos: undefined, + }); + + readonly areEmulatorsRunning = computed(() => { + return this.emulators.value.status === "running"; + }); + + private readonly subscriptions: (() => void)[] = []; + + /** + * Formats a project ID with a demo prefix if we're in offline mode, or uses the + * regular ID if we're in dataconnect only mode. + */ + private getProjectIdForMode( + projectId: string | undefined, + mode: EmulatorUiSelections["mode"], + ): string { + if (!projectId) { + return "demo-something"; + } + if (mode === "dataconnect") { + return projectId; + } + return "demo-" + projectId; + } + + notifyUISelectionChangedListeners() { + this.broker.send( + "notifyEmulatorUiSelectionsChanged", + this.uiSelections.value, + ); + } + + notifyEmulatorStateChanged() { + this.broker.send("notifyEmulatorStateChanged", this.emulators.value); + } + + async waitEmulators() { + await this.pendingEmulatorStart; + } + + async startEmulators() { + this.emulators.value = { + status: "starting", + infos: this.emulators.value.infos, + }; + + const currentOp = (this.pendingEmulatorStart = new Promise(async () => { + try { + await emulatorsStart(this.uiSelections.value); + this.emulators.value = { + status: "running", + infos: { + displayInfo: listRunningEmulators(), + }, + }; + // TODO: Add other emulator icons + this.emulatorStatusItem.text = "$(data-connect) Emulators: Running"; + + // Updating the status bar label as "running", but don't "show" it. + // We only show the status bar item when explicitly by interacting with the sidebar. + this.emulatorStatusItem.text = "$(data-connect) Emulators: Running"; + this.emulatorStatusItem.backgroundColor = undefined; + } catch (e) { + this.emulatorStatusItem.text = "$(data-connect) Emulators: errored"; + this.emulatorStatusItem.backgroundColor = new ThemeColor( + "statusBarItem.errorBackground", + ); + this.emulatorStatusItem.show(); + this.emulators.value = { + status: "stopped", + infos: undefined, + }; + } + + if (currentOp === this.pendingEmulatorStart) { + this.pendingEmulatorStart = undefined; + } + })); + + return currentOp; + } + + async stopEmulators() { + this.emulators.value = { + status: "stopping", + infos: this.emulators.value.infos, + }; + await stopEmulators(); + this.emulators.value = { + status: "stopped", + infos: undefined, + }; + } + + // TODO: Move all api calls to CLI DataConnectEmulatorClient + public getLocalEndpoint = () => computed(() => { + const emulatorInfos = this.emulators.value.infos?.displayInfo; + const dataConnectEmulator = emulatorInfos?.find( + (emulatorInfo) => emulatorInfo.name === Emulators.DATACONNECT, + ); + + if (!dataConnectEmulator) { + return undefined; + } + + // handle ipv6 + if (dataConnectEmulator.host.includes(":")) { + return `http://[${dataConnectEmulator.host}]:${dataConnectEmulator.port}`; + } + return `http://${dataConnectEmulator.host}:${dataConnectEmulator.port}`; + }); + + dispose(): void { + this.stopEmulators(); + this.subscriptions.forEach((subscription) => subscription()); + this.waitCommand.dispose(); + this.emulatorStatusItem.dispose(); + } +} diff --git a/firebase-vscode/src/core/env.ts b/firebase-vscode/src/core/env.ts new file mode 100644 index 00000000000..2d9701be7d2 --- /dev/null +++ b/firebase-vscode/src/core/env.ts @@ -0,0 +1,26 @@ +import { Disposable } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { pluginLogger } from "../logger-wrapper"; +import { globalSignal } from "../utils/globals"; + +interface Environment { + isMonospace: boolean; +} + +export const env = globalSignal({ + isMonospace: Boolean(process.env.MONOSPACE_ENV), +}); + +export function registerEnv(broker: ExtensionBrokerImpl): Disposable { + const sub = broker.on("getInitialData", async () => { + pluginLogger.debug( + `Value of process.env.MONOSPACE_ENV: ` + `${process.env.MONOSPACE_ENV}` + ); + + broker.send("notifyEnv", { + env: env.peek(), + }); + }); + + return { dispose: sub }; +} diff --git a/firebase-vscode/src/core/index.ts b/firebase-vscode/src/core/index.ts new file mode 100644 index 00000000000..ddec9c62886 --- /dev/null +++ b/firebase-vscode/src/core/index.ts @@ -0,0 +1,84 @@ +import vscode, { Disposable, ExtensionContext, TelemetryLogger } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { getRootFolders, registerConfig } from "./config"; +import { EmulatorsController } from "./emulators"; +import { registerEnv } from "./env"; +import { pluginLogger } from "../logger-wrapper"; +import { getSettings } from "../utils/settings"; +import { setEnabled } from "../../../src/experiments"; +import { registerUser } from "./user"; +import { registerProject } from "./project"; +import { registerQuickstart } from "./quickstart"; +import { registerOptions } from "../options"; +import { upsertFile } from "../data-connect/file-utils"; + +export async function registerCore( + broker: ExtensionBrokerImpl, + context: ExtensionContext, + telemetryLogger: TelemetryLogger, +): Promise<[EmulatorsController, vscode.Disposable]> { + const settings = getSettings(); + + if (settings.npmPath) { + process.env.PATH += `:${settings.npmPath}`; + } + + if (settings.useFrameworks) { + setEnabled("webframeworks", true); + } + + const sub1 = broker.on("writeLog", async ({ level, args }) => { + pluginLogger[level]("(Webview)", ...args); + }); + + const sub2 = broker.on("showMessage", async ({ msg, options }) => { + vscode.window.showInformationMessage(msg, options); + }); + + const sub3 = broker.on("openLink", async ({ href }) => { + vscode.env.openExternal(vscode.Uri.parse(href)); + }); + + const sub4 = broker.on("runFirebaseInit", async () => { + vscode.tasks.executeTask( + new vscode.Task( + { type: "shell" }, // this is the same type as in tasks.json + vscode.workspace.workspaceFolders[0], // The workspace folder + "Firebase init", // how you name the task + "Firebase init", // Shows up as MyTask: name + new vscode.ShellExecution("firebase init"), + ), + ); + }); + + const emulatorsController = new EmulatorsController(broker); + // Start the emulators when the extension starts. + emulatorsController.startEmulators(); + + const openRcCmd = vscode.commands.registerCommand( + "firebase.openFirebaseRc", + () => { + for (const root of getRootFolders()) { + upsertFile(vscode.Uri.file(`${root}/.firebaserc`), () => ""); + } + }, + ); + + return [ + emulatorsController, + Disposable.from( + openRcCmd, + emulatorsController, + registerOptions(context), + registerConfig(broker), + registerEnv(broker), + registerUser(broker, telemetryLogger), + registerProject(broker), + registerQuickstart(broker), + { dispose: sub1 }, + { dispose: sub2 }, + { dispose: sub3 }, + { dispose: sub4 }, + ), + ]; +} diff --git a/firebase-vscode/src/core/project.ts b/firebase-vscode/src/core/project.ts new file mode 100644 index 00000000000..5c0fb5dd7fc --- /dev/null +++ b/firebase-vscode/src/core/project.ts @@ -0,0 +1,144 @@ +import vscode, { Disposable } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { computed, effect } from "@preact/signals-react"; +import { firebaseRC, updateFirebaseRCProject } from "./config"; +import { FirebaseProjectMetadata } from "../types/project"; +import { currentUser, isServiceAccount } from "./user"; +import { listProjects } from "../cli"; +import { pluginLogger } from "../logger-wrapper"; +import { globalSignal } from "../utils/globals"; +import { firstWhereDefined } from "../utils/signal"; +import { User } from "../types/auth"; + +/** Available projects */ +export const projects = globalSignal>( + {}, +); + +/** Currently selected project ID */ +export const currentProjectId = globalSignal(""); + +const userScopedProjects = computed( + () => { + return projects.value[currentUser.value?.email ?? ""]; + }, +); + +/** Gets the currently selected project, fallback to first default project in RC file */ +export const currentProject = computed( + () => { + // Service accounts should only have one project + if (isServiceAccount.value) { + return userScopedProjects.value?.[0]; + } + + const wantProjectId = + currentProjectId.value || + firebaseRC.value?.tryReadValue?.projects["default"]; + if (!wantProjectId) { + return undefined; + } + + return userScopedProjects.value?.find((p) => p.projectId === wantProjectId); + }, +); + +export function registerProject(broker: ExtensionBrokerImpl): Disposable { + + async function fetchNewProjects(user: User) { + const userProjects = await listProjects(); + projects.value = { + ...projects.value, + [user.email]: userProjects, + }; + } + + const sub1 = effect(() => { + const user = currentUser.value; + if (user) { + pluginLogger.info("(Core:Project) New user detected, fetching projects"); + fetchNewProjects(user); + } + }); + + const sub2 = effect(() => { + broker.send("notifyProjectChanged", { + projectId: currentProject.value?.projectId ?? "", + }); + }); + + // Update .firebaserc with defined project ID + const sub3 = effect(() => { + const projectId = currentProjectId.value; + if (projectId) { + updateFirebaseRCProject({ + projectAlias: { alias: "default", projectId }, + }); + } + }); + + // Initialize currentProjectId to default project ID + const sub4 = effect(() => { + if (!currentProjectId.value) { + currentProjectId.value = firebaseRC.value?.tryReadValue?.projects.default; + } + }); + + const sub5 = broker.on("getInitialData", () => { + broker.send("notifyProjectChanged", { + projectId: currentProject.value?.projectId ?? "", + }); + }); + + const command = vscode.commands.registerCommand( + "firebase.selectProject", + async () => { + if (isServiceAccount.value) { + return; + } else { + try { + const projects = firstWhereDefined(userScopedProjects); + + currentProjectId.value = + (await _promptUserForProject(projects)) ?? currentProjectId.value; + } catch (e) { + vscode.window.showErrorMessage(e.message); + } + } + }, + ); + + const sub6 = broker.on("selectProject", () => + vscode.commands.executeCommand("firebase.selectProject"), + ); + + return vscode.Disposable.from( + command, + { dispose: sub1 }, + { dispose: sub2 }, + { dispose: sub3 }, + { dispose: sub4 }, + { dispose: sub5 }, + { dispose: sub6 }, + ); +} + +/** + * Get the user to select a project + * + * @internal + */ +export async function _promptUserForProject( + projects: Thenable, + token?: vscode.CancellationToken, +): Promise { + const items = projects.then((projects) => { + return projects.map((p) => ({ + label: p.projectId, + description: p.displayName, + })); + }); + + const item = await vscode.window.showQuickPick(items, {}, token); + return item?.label; +} diff --git a/firebase-vscode/src/core/quickstart.ts b/firebase-vscode/src/core/quickstart.ts new file mode 100644 index 00000000000..daa00cb351f --- /dev/null +++ b/firebase-vscode/src/core/quickstart.ts @@ -0,0 +1,54 @@ +import vscode, { Disposable } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { pluginLogger } from "../logger-wrapper"; +import { execSync } from "child_process"; + +export function registerQuickstart(broker: ExtensionBrokerImpl): Disposable { + const sub = broker.on("chooseQuickstartDir", selectDirectory); + + return { dispose: sub }; +} + +// Opens a dialog prompting the user to select a directory. +// @returns string file path with directory location +async function selectDirectory() { + const selectedURI = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + }); + + /** + * If the user did not prematurely close the dialog and a directory in + * which to put the new quickstart was selected, execute a sequence of + * shell commands that: + * 1. Downloads the quickstart into the selected directory with `git clone` + * 2. Enters the downloaded repo and deletes all unnecessary files and dirs + * 3. Moves all remaining files to the root of the selected directory + * + * Once this download and configuration is complete, a new vscode window + * is opened to the selected directory. + */ + if (selectedURI && selectedURI[0]) { + pluginLogger.info("(Quickstart) Downloading Quickstart Project"); + try { + pluginLogger.info( + execSync( + `git clone https://github.com/firebase/quickstart-js.git ` + + `&& cd quickstart-js && ls | grep -xv "firestore" | xargs rm -rf ` + + `&& mv -v firestore/* "${selectedURI[0].fsPath}" ` + + `&& cd "${selectedURI[0].fsPath}" && rm -rf quickstart-js`, + { + cwd: selectedURI[0].fsPath, + encoding: "utf8", + } + ) + ); + vscode.commands.executeCommand(`vscode.openFolder`, selectedURI[0]); + } catch (error) { + pluginLogger.error( + "(Quickstart) Error downloading Quickstart:\n" + error + ); + } + } +} diff --git a/firebase-vscode/src/core/user.ts b/firebase-vscode/src/core/user.ts new file mode 100644 index 00000000000..001393ff755 --- /dev/null +++ b/firebase-vscode/src/core/user.ts @@ -0,0 +1,85 @@ +import { computed, effect } from "@preact/signals-react"; +import { Disposable, TelemetryLogger } from "vscode"; +import { ServiceAccountUser } from "../types"; +import { User as AuthUser } from "../../../src/types/auth"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { getAccounts, login, logoutUser } from "../cli"; +import { globalSignal } from "../utils/globals"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; + +type User = ServiceAccountUser | AuthUser; + +/** Available user accounts */ +export const users = globalSignal>({}); + +/** Currently selected user email */ +export const currentUserId = globalSignal(""); + +/** Gets the currently selected user, fallback to first available user */ +export const currentUser = computed(() => { + return users.value[currentUserId.value] ?? Object.values(users.value)[0]; +}); + +export const isServiceAccount = computed(() => { + return (currentUser.value as ServiceAccountUser)?.type === "service_account"; +}); + +export async function checkLogin() { + const accounts = await getAccounts(); + users.value = accounts.reduce( + (cumm, curr) => ({ ...cumm, [curr.user.email]: curr.user }), + {} + ); +} + +export function registerUser(broker: ExtensionBrokerImpl, telemetryLogger: TelemetryLogger): Disposable { + + const sub1 = effect(() => { + broker.send("notifyUsers", { users: Object.values(users.value) }); + }); + + const sub2 = effect(() => { + broker.send("notifyUserChanged", { user: currentUser.value }); + }); + + const sub3 = broker.on("getInitialData", async () => { + checkLogin(); + }); + + const sub4 = broker.on("addUser", async () => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.LOGIN); + const { user } = await login(); + users.value = { + ...users.value, + [user.email]: user, + }; + currentUserId.value = user.email; + }); + + const sub5 = broker.on("requestChangeUser", ({ user }) => { + currentUserId.value = user.email; + }); + + const sub6 = broker.on("logout", async ({ email }) => { + try { + await logoutUser(email); + const accounts = await getAccounts(); + users.value = accounts.reduce( + (cumm, curr) => ({ ...cumm, [curr.user.email]: curr.user }), + {} + ); + currentUserId.value = ""; + } catch (e) { + // ignored + } + }); + + return Disposable.from( + { dispose: sub1 }, + { dispose: sub2 }, + { dispose: sub3 }, + { dispose: sub4 }, + { dispose: sub5 }, + { dispose: sub6 } + ); +} diff --git a/firebase-vscode/src/data-connect/ad-hoc-mutations.ts b/firebase-vscode/src/data-connect/ad-hoc-mutations.ts new file mode 100644 index 00000000000..7c4ede6eeed --- /dev/null +++ b/firebase-vscode/src/data-connect/ad-hoc-mutations.ts @@ -0,0 +1,204 @@ +import vscode, { Disposable, TelemetryLogger } from "vscode"; +import { DocumentNode, GraphQLInputObjectType, GraphQLScalarType, Kind, ObjectTypeDefinitionNode, buildClientSchema, buildSchema } from "graphql"; +import { checkIfFileExists, upsertFile } from "./file-utils"; +import { DataConnectService } from "./service"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; + +export function registerAdHoc(dataConnectService: DataConnectService, telemetryLogger: TelemetryLogger): Disposable { + const defaultScalarValues = { + Any: "{}", + AuthUID: '""', + Boolean: "false", + Date: `"${new Date().toISOString().substring(0, 10)}"`, + Float: "0", + ID: '""', + Int: "0", + Int64: "0", + String: '""', + Timestamp: `"${new Date().toISOString()}"`, + Vector: "[]", + }; + + function isDataConnectScalarType(fieldType: string): boolean { + return fieldType in defaultScalarValues; + } + + /** + * Creates a playground file with an ad-hoc mutation + * File will be created (unsaved) in operations/ folder, with an auto-generated named based on the schema type + * Mutation will be generated with all + * */ + async function schemaReadData( + document: DocumentNode, + ast: ObjectTypeDefinitionNode, + ) { + // TODO(rrousselGit) - this is a temporary solution due to the lack of a "schema". + // As such, we hardcoded the list of allowed primitives. + // We should ideally refactor this to allow any scalar type. + const primitiveTypes = new Set([ + "String", + "Int", + "Int64", + "Boolean", + "Date", + "Timestamp", + "Float", + "Any", + ]); + + const basePath = vscode.workspace.rootPath + "/dataconnect/"; + const filePath = vscode.Uri.file(`${basePath}${ast.name.value}_read.gql`); + + // Recursively build a query for the object type. + // Returns undefined if the query is empty. + function buildRecursiveObjectQuery( + ast: ObjectTypeDefinitionNode, + level: number = 1, + ): string | undefined { + const indent = " ".repeat(level); + + // Whether the query is non-empty. Used to determine whether to return undefined. + var hasField = false; + let query = "{\n"; + for (const field of ast.fields) { + // We unwrap NonNullType to obtain the actual type + let fieldType = field.type; + if (fieldType.kind === Kind.NON_NULL_TYPE) { + fieldType = fieldType.type; + } + + // Deference, for the sake of enabling TS to upcast to NamedType later + const targetType = fieldType; + if (targetType.kind === Kind.NAMED_TYPE) { + // Check if the type is a primitive type, such that no recursion is needed. + if (primitiveTypes.has(targetType.name.value)) { + query += ` ${indent}${field.name.value}\n`; + hasField = true; + continue; + } + + // Check relational types. + // Since we lack a schema, we can only build queries for types that are defined in the same document. + const targetTypeDefinition = document.definitions.find( + (def) => + def.kind === Kind.OBJECT_TYPE_DEFINITION && + def.name.value === targetType.name.value, + ) as ObjectTypeDefinitionNode; + + if (targetTypeDefinition) { + const subQuery = buildRecursiveObjectQuery( + targetTypeDefinition, + level + 1, + ); + if (!subQuery) { + continue; + } + query += ` ${indent}${field.name.value} ${subQuery}\n`; + hasField = true; + } + } + } + + query += `${indent}}`; + return query; + } + + await upsertFile(filePath, () => { + const queryName = `${ast.name.value.charAt(0).toLowerCase()}${ast.name.value.slice(1)}s`; + + return ` +# This is a file for you to write an un-named queries. +# Only one un-named query is allowed per file. +query { + ${queryName}${buildRecursiveObjectQuery(ast)!} +}`; + }); + } + + /** + * Creates a playground file with an ad-hoc mutation + * File will be created (unsaved) in operations/ folder, with an auto-generated named based on the schema type + * Mutation will be generated with all + * */ + async function schemaAddData(ast: ObjectTypeDefinitionNode) { + // generate content for the file + const preamble = + "# This is a file for you to write an un-named mutation. \n# Only one un-named mutation is allowed per file."; + const adhocMutation = await generateMutation(ast); + const content = [preamble, adhocMutation].join("\n"); + + const basePath = vscode.workspace.rootPath + "/dataconnect/"; + const filePath = vscode.Uri.file(`${basePath}${ast.name.value}_insert.gql`); + const doesFileExist = await checkIfFileExists(filePath); + + if (!doesFileExist) { + // opens unsaved text document with name "[mutationName]_insert.gql" + + vscode.workspace + .openTextDocument(filePath.with({ scheme: "untitled" })) + .then((doc) => { + vscode.window.showTextDocument(doc).then((openDoc) => { + openDoc.edit((edit) => { + edit.insert(new vscode.Position(0, 0), content); + }); + }); + }); + } else { + // Opens existing text document + vscode.workspace.openTextDocument(filePath).then((doc) => { + vscode.window.showTextDocument(doc); + }); + } + } + + async function generateMutation( + ast: ObjectTypeDefinitionNode, + ): Promise { + const introspect = (await dataConnectService.introspect())?.data; + const schema = buildClientSchema(introspect); + + const name = ast.name.value; + const lowerCaseName = + ast.name.value.charAt(0).toLowerCase() + ast.name.value.slice(1); + const dataName = `${name}_Data`; + const mutationDataType: GraphQLInputObjectType = schema.getTypeMap()[dataName] as GraphQLInputObjectType; + + // build mutation as string + const functionSpacing = "\t"; + const fieldSpacing = "\t\t"; + const mutation = []; + mutation.push("mutation {"); // mutation header + mutation.push(`${functionSpacing}${lowerCaseName}_insert(data: {`); + for (const [fieldName, field] of Object.entries(mutationDataType.getFields())) { + // necessary to avoid type error + const fieldtype: any = field.type; + // use all argument types that are of scalar, except x_expr + if (isDataConnectScalarType(fieldtype.name) && !field.name.includes("_expr")) { + const defaultValue = defaultScalarValues[fieldtype.name] || ""; + mutation.push( + `${fieldSpacing}${fieldName}: ${defaultValue} # ${fieldtype.name}`, + ); // field name + temp value + comment + } + + } + mutation.push(`${functionSpacing}})`, "}"); // closing braces/paren + return mutation.join("\n"); + } + + return Disposable.from( + vscode.commands.registerCommand( + "firebase.dataConnect.schemaAddData", + (ast) => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.ADD_DATA); + schemaAddData(ast); + }, + ), + vscode.commands.registerCommand( + "firebase.dataConnect.schemaReadData", + (document, ast) => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.READ_DATA); + schemaReadData(document, ast); + }, + ), + ); +} diff --git a/firebase-vscode/src/data-connect/code-lens-provider.ts b/firebase-vscode/src/data-connect/code-lens-provider.ts new file mode 100644 index 00000000000..4c75f4e1569 --- /dev/null +++ b/firebase-vscode/src/data-connect/code-lens-provider.ts @@ -0,0 +1,178 @@ +import * as vscode from "vscode"; +import { Kind, parse } from "graphql"; +import { OperationLocation } from "./types"; +import { Disposable } from "vscode"; + +import { Signal } from "@preact/signals-core"; +import { dataConnectConfigs, firebaseRC } from "./config"; +import { EmulatorsController } from "../core/emulators"; +import { DataConnectEmulatorController } from "./emulator"; + +export enum InstanceType { + LOCAL = "local", + PRODUCTION = "production", +} + +abstract class ComputedCodeLensProvider implements vscode.CodeLensProvider { + private readonly _onChangeCodeLensesEmitter = new vscode.EventEmitter(); + onDidChangeCodeLenses = this._onChangeCodeLensesEmitter.event; + + private readonly subscriptions: Map, Disposable> = new Map(); + + watch(signal: Signal): T { + if (!this.subscriptions.has(signal)) { + let initialFire = true; + const disposable = signal.subscribe(() => { + // Signals notify their listeners immediately, even if no change were detected. + // This is undesired here as such notification would be picked up by vscode, + // triggering an infinite reload loop of the codelenses. + // We therefore skip this notification and only keep actual "change" notifications + if (initialFire) { + initialFire = false; + return; + } + + this._onChangeCodeLensesEmitter.fire(); + }); + + this.subscriptions.set(signal, { dispose: disposable }); + } + + return signal.peek(); + } + + dispose() { + for (const disposable of this.subscriptions.values()) { + disposable.dispose(); + } + this.subscriptions.clear(); + } + + abstract provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken, + ): vscode.CodeLens[]; +} + +/** + * CodeLensProvider provides codelens for actions in graphql files. + */ +export class OperationCodeLensProvider extends ComputedCodeLensProvider { + constructor(readonly emulatorsController: DataConnectEmulatorController) { + super(); + } + + provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken, + ): vscode.CodeLens[] { + // Wait for configs to be loaded and emulator to be running + const fdcConfigs = this.watch(dataConnectConfigs)?.tryReadValue; + const rc = this.watch(firebaseRC)?.tryReadValue; + if (!fdcConfigs || !rc) { + return []; + } + + const codeLenses: vscode.CodeLens[] = []; + + const documentText = document.getText(); + // TODO: replace w/ online-parser to work with malformed documents + const documentNode = parse(documentText); + + for (let i = 0; i < documentNode.definitions.length; i++) { + const x = documentNode.definitions[i]; + if (x.kind === Kind.OPERATION_DEFINITION && x.loc) { + const line = x.loc.startToken.line - 1; + const range = new vscode.Range(line, 0, line, 0); + const position = new vscode.Position(line, 0); + const operationLocation: OperationLocation = { + document: documentText, + documentPath: document.fileName, + position: position, + }; + const service = fdcConfigs.findEnclosingServiceForPath( + document.fileName, + ); + + if (service) { + if (this.watch(this.emulatorsController.isPostgresEnabled)) { + codeLenses.push( + new vscode.CodeLens(range, { + title: `$(play) Run (local)`, + command: "firebase.dataConnect.executeOperation", + tooltip: "Execute the operation (⌘+enter or Ctrl+Enter)", + arguments: [x, operationLocation, InstanceType.LOCAL], + }), + ); + } + + codeLenses.push( + new vscode.CodeLens(range, { + title: `$(play) Run (Production – Project: ${rc.projects.default})`, + command: "firebase.dataConnect.executeOperation", + tooltip: "Execute the operation (⌘+enter or Ctrl+Enter)", + arguments: [x, operationLocation, InstanceType.PRODUCTION], + }), + ); + } + } + } + + return codeLenses; + } +} + +/** + * CodeLensProvider for actions on the schema file + */ +export class SchemaCodeLensProvider extends ComputedCodeLensProvider { + constructor(readonly emulatorsController: EmulatorsController) { + super(); + } + + provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken, + ): vscode.CodeLens[] { + if (!this.watch(this.emulatorsController.areEmulatorsRunning)) { + return []; + } + + const codeLenses: vscode.CodeLens[] = []; + + // TODO: replace w/ online-parser to work with malformed documents + const documentNode = parse(document.getText()); + + for (const x of documentNode.definitions) { + if (x.kind === Kind.OBJECT_TYPE_DEFINITION && x.loc) { + const line = x.loc.startToken.line - 1; + const range = new vscode.Range(line, 0, line, 0); + const position = new vscode.Position(line, 0); + const schemaLocation = { + documentPath: document.fileName, + position: position, + }; + + codeLenses.push( + new vscode.CodeLens(range, { + title: `$(database) Add data`, + command: "firebase.dataConnect.schemaAddData", + tooltip: "Generate a mutation to add data of this type", + arguments: [x, schemaLocation], + }), + ); + + codeLenses.push( + new vscode.CodeLens(range, { + title: `$(database) Read data`, + command: "firebase.dataConnect.schemaReadData", + tooltip: "Generate a query to read data of this type", + arguments: [documentNode, x], + }), + ); + } + } + + return codeLenses; + } +} diff --git a/firebase-vscode/src/data-connect/config.ts b/firebase-vscode/src/data-connect/config.ts new file mode 100644 index 00000000000..eaf87626647 --- /dev/null +++ b/firebase-vscode/src/data-connect/config.ts @@ -0,0 +1,223 @@ +import { isPathInside } from "./file-utils"; +import { DeepReadOnly } from "../metaprogramming"; +import { ConnectorYaml, DataConnectYaml } from "../dataconnect/types"; +import { Result, ResultValue } from "../result"; +import { computed, effect, signal } from "@preact/signals-core"; +import { + _createWatcher as createWatcher, + firebaseConfig, + getConfigPath, +} from "../core/config"; +import * as vscode from "vscode"; +import * as promise from "../utils/promise"; +import { + readConnectorYaml, + readDataConnectYaml, + readFirebaseJson as readFdcFirebaseJson, +} from "../../../src/dataconnect/fileUtils"; +import { Config } from "../config"; +import { DataConnectMultiple } from "../firebaseConfig"; +import path from "path"; +import { ExtensionBrokerImpl } from "../extension-broker"; + +export * from "../core/config"; + +export const dataConnectConfigs = signal< + Result | undefined +>(undefined); + +export function registerDataConnectConfigs( + broker: ExtensionBrokerImpl, +): vscode.Disposable { + let cancel: () => void | undefined; + + function handleResult( + firebaseConfig: Result | undefined, + ) { + cancel?.(); + cancel = undefined; + + // While waiting for the promise to resolve, we clear the configs, to tell anything that depends + // on it that it's loading. + dataConnectConfigs.value = undefined; + + const configs = firebaseConfig?.followAsync( + async (config) => + new ResultValue( + await _readDataConnectConfigs(readFdcFirebaseJson(config)), + ), + ); + + cancel = + configs && + promise.cancelableThen( + configs, + (configs) => (dataConnectConfigs.value = configs.tryReadValue), + ).cancel; + } + + const sub = effect(() => handleResult(firebaseConfig.value)); + + const dataConnectWatcher = createWatcher("**/{dataconnect,connector}.yaml"); + dataConnectWatcher?.onDidChange(() => handleResult(firebaseConfig.value)); + dataConnectWatcher?.onDidCreate(() => handleResult(firebaseConfig.value)); + dataConnectWatcher?.onDidDelete(() => handleResult(undefined)); + // TODO watch connectors + + const hasConfigs = computed(() => !!dataConnectConfigs.value?.tryReadValue?.values.length); + + const hasConfigSub = effect(() => { + broker.send("notifyHasFdcConfigs", hasConfigs.value); + }); + const getInitialHasFdcConfigsSub = broker.on("getInitialHasFdcConfigs", () => { + broker.send("notifyHasFdcConfigs", hasConfigs.value); + }); + + return vscode.Disposable.from( + { dispose: sub }, + { dispose: hasConfigSub }, + { dispose: getInitialHasFdcConfigsSub }, + { dispose: () => cancel?.() }, + dataConnectWatcher, + ); +} + +/** @internal */ +export async function _readDataConnectConfigs( + fdcConfig: DataConnectMultiple, +): Promise> { + return Result.guard(async () => { + const dataConnects = await Promise.all( + fdcConfig.map>(async (dataConnect) => { + // Paths may be relative to the firebase.json file. + const absoluteLocation = asAbsolutePath( + dataConnect.source, + getConfigPath(), + ); + const dataConnectYaml = await readDataConnectYaml(absoluteLocation); + const resolvedConnectors = await Promise.all( + dataConnectYaml.connectorDirs.map((connectorDir) => + Result.guard(async () => { + const connectorYaml = await readConnectorYaml( + // Paths may be relative to the dataconnect.yaml + asAbsolutePath(connectorDir, absoluteLocation), + ); + return new ResolvedConnectorYaml( + asAbsolutePath(connectorDir, absoluteLocation), + connectorYaml, + ); + }), + ), + ); + return new ResolvedDataConnectConfig( + absoluteLocation, + dataConnectYaml, + resolvedConnectors, + dataConnectYaml.location, + ); + }), + ); + return new ResolvedDataConnectConfigs(dataConnects); + }); +} + +function asAbsolutePath(relativePath: string, from: string): string { + return path.normalize(path.join(from, relativePath)); +} + +export class ResolvedConnectorYaml { + constructor( + readonly path: string, + readonly value: DeepReadOnly + ) {} + + containsPath(path: string) { + return isPathInside(path, this.path); + } +} + +export class ResolvedDataConnectConfig { + constructor( + readonly path: string, + readonly value: DeepReadOnly, + readonly resolvedConnectors: Result[], + readonly dataConnectLocation: string, + ) {} + + get connectorIds(): string[] { + const result: string[] = []; + + for (const connector of this.resolvedConnectors) { + const id = connector.tryReadValue?.value.connectorId; + if (id) { + result.push(id); + } + } + + return result; + } + + get connectorDirs(): string[] { + return this.value.connectorDirs; + } + + get schemaDir(): string { + return this.value.schema.source; + } + + get relativePath(): string { + return this.path.split("/").pop(); + } + + get relativeSchemaPath(): string { + return this.schemaDir.replace(".", this.relativePath); + } + + get relativeConnectorPaths(): string[] { + return this.connectorDirs.map((connectorDir) => connectorDir.replace(".", this.relativePath)); + } + + containsPath(path: string) { + return isPathInside(path, this.path); + } + + findEnclosingConnectorForPath(filePath: string) { + return this.resolvedConnectors.find( + (connector) => connector.tryReadValue?.containsPath(filePath) ?? false, + ); + } +} + +/** The fully resolved `dataconnect.yaml` and its connectors */ +export class ResolvedDataConnectConfigs { + constructor(readonly values: DeepReadOnly) {} + + get serviceIds() { + return this.values.map((config) => config.value.serviceId); + } + + get allConnectors() { + return this.values.flatMap((dc) => dc.resolvedConnectors); + } + + findById(serviceId: string) { + return this.values.find((dc) => dc.value.serviceId === serviceId); + } + + findEnclosingServiceForPath(filePath: string) { + return this.values.find((dc) => dc.containsPath(filePath)); + } + + getApiServicePathByPath(projectId: string, path: string) { + const dataConnectConfig = this.findEnclosingServiceForPath(path); + const serviceId = dataConnectConfig.value.serviceId; + const locationId = dataConnectConfig.dataConnectLocation; + + return `projects/${projectId}/locations/${locationId}/services/${serviceId}`; + } +} + +// TODO: Expand this into a VSCode env config object/class +export enum VSCODE_ENV_VARS { + DATA_CONNECT_ORIGIN = "FIREBASE_DATACONNECT_URL", +} diff --git a/firebase-vscode/src/data-connect/connectors.ts b/firebase-vscode/src/data-connect/connectors.ts new file mode 100644 index 00000000000..314b9e2ee5c --- /dev/null +++ b/firebase-vscode/src/data-connect/connectors.ts @@ -0,0 +1,522 @@ +import vscode, { + Disposable, + ExtensionContext, + InputBoxValidationMessage, + InputBoxValidationSeverity, + TelemetryLogger, +} from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { + ASTNode, + ArgumentNode, + ConstValueNode, + DocumentNode, + GraphQLInputType, + GraphQLLeafType, + GraphQLNonNull, + IntrospectionQuery, + Kind, + NamedTypeNode, + ObjectFieldNode, + OperationDefinitionNode, + Source, + TypeInfo, + TypeNode, + VariableNode, + buildClientSchema, + isConstValueNode, + isEnumType, + isLeafType, + isNonNullType, + parse, + print, + separateOperations, + visit, + visitWithTypeInfo, +} from "graphql"; +import { camelCase } from "lodash"; +import { DataConnectService } from "./service"; +import { OperationLocation } from "./types"; +import { checkIfFileExists } from "./file-utils"; +import * as path from "path"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; + +export function registerConnectors( + context: ExtensionContext, + broker: ExtensionBrokerImpl, + dataConnectService: DataConnectService, + telemetryLogger: TelemetryLogger, +): Disposable { + async function moveOperationToConnector( + defIndex: number, // The index of the definition to move. + { documentPath, document }: OperationLocation, + connectorPath: string, + ) { + const ast = parse(new Source(document, documentPath)); + + const def = ast.definitions[defIndex]; + if (!def) { + throw new Error(`definitions[${defIndex}] not found.`); + } + if (def.kind !== Kind.OPERATION_DEFINITION) { + throw new Error(`definitions[${defIndex}] is not an operation.`); + } + const introspect = (await dataConnectService.introspect())?.data; + if (!introspect) { + vscode.window.showErrorMessage( + "Failed to introspect the types. (Is the emulator running?)", + ); + return; + } + const opKind = def.operation as string; // query or mutation + + let opName = def.name?.value; + if (!opName || (await validateOpName(opName)) !== null) { + opName = await vscode.window.showInputBox({ + title: `Pick a name for the ${opKind}`, + placeHolder: `e.g. ${camelCase("my-" + opKind)}`, + prompt: `Name of the ${opKind} (to be used with SDKs).`, + value: opName || suggestOpName(def, documentPath), + validateInput: validateOpName, + }); + + if (!opName) { + return; // Dialog dismissed by the developer. + } + } + + // While `parse` above tolerates operations with duplicate names (or + // multiple anonymous operations), `separateOperations` will misbehave. + // So we reassign the names to be all unique just in case. + let i = 0; + const opAst = separateOperations( + visit(ast, { + OperationDefinition(node) { + i++; + return { + ...node, + name: { + kind: Kind.NAME, + value: node === def ? opName : `ignored${i}`, + }, + }; + }, + }), + )[opName]; + // opAst contains only the operation we care about plus fragments used. + if (!opAst) { + throw new Error("Error separating operations."); + } + + const candidates = findExtractCandidates(opAst, introspect); + + const picked = await vscode.window.showQuickPick(candidates, { + title: `Extract variables that can be modified by clients`, + placeHolder: `(type to filter...)`, + canPickMany: true, + ignoreFocusOut: true, + }); + + if (!picked) { + return; // Dialog dismissed by the developer. + } + + const newAst = extractVariables(opAst, picked); + const content = print(newAst); + const filePath = getFilePath(opName); + + vscode.workspace + .openTextDocument(filePath.with({ scheme: "untitled" })) + .then((doc) => { + vscode.window.showTextDocument(doc).then((openDoc) => { + openDoc.edit((edit) => { + edit.insert(new vscode.Position(0, 0), content); + }); + }); + }); + + // TODO: Consider removing the operation from the original document? + + vscode.window.showInformationMessage( + `Moved ${opName} to ${vscode.workspace.asRelativePath(filePath)}`, + ); + + async function validateOpName( + value: string, + ): Promise { + if (!value) { + return { + severity: InputBoxValidationSeverity.Error, + message: `A name is required for each ${opKind} in a connector.`, + }; + } + // TODO: Check if an operation with the same name exists in basePath. + const fp = getFilePath(value); + + if (await checkIfFileExists(fp)) { + return { + // We're treating this as fatal under the assumption that the file may + // contain an operation with the same name. Once we can actually rule + // out naming conflicts above, we should handle this better, such as + // appending to that file or choosing a different file like xxx2.gql. + severity: InputBoxValidationSeverity.Error, + message: `${vscode.workspace.asRelativePath(fp)} already exists.`, + }; + } + } + + function getFilePath(opName: string) { + return vscode.Uri.file(path.join(connectorPath, `${opName}.gql`)); + } + } + + function suggestOpName(ast: OperationDefinitionNode, documentPath: string) { + if (documentPath) { + // Suggest name from basename (e.g. /foo/bar/baz_quax.gql => bazQuax). + const match = documentPath.match(/([^./\\]+)\./); + if (match) { + return camelCase(match[1]); + } + } + for (const sel of ast.selectionSet.selections) { + if (sel.kind === Kind.FIELD) { + // Suggest name from the first field (e.g. foo_insert => fooInsert). + return camelCase(sel.name.value); + } + } + return camelCase(`my-${ast.operation}-${Math.floor(Math.random() * 100)}`); + } + + function findExtractCandidates( + ast: DocumentNode, + introspect: IntrospectionQuery, + ): ExtractCandidate[] { + const candidates: ExtractCandidate[] = []; + const seenVarNames = new Set(); + visit(ast, { + VariableDefinition(node) { + seenVarNames.add(node.variable.name.value); + }, + }); + // TODO: Make this work for inline and non-inline fragments. + const fieldPath: string[] = []; + let directiveName: string | undefined = undefined; + let argName: string | undefined = undefined; + const valuePath: string[] = []; + const schema = buildClientSchema(introspect, { assumeValid: true }); + const typeInfo = new TypeInfo(schema); + // Visits operations as well as fragments. + visit( + ast, + visitWithTypeInfo(typeInfo, { + VariableDefinition() { + // Do not extract literals in variable default values or directives. + return false; + }, + Directive: { + enter(node) { + if (node.name.value === "auth") { + // Auth should not be modifiable by clients. + return false; + } + // @skip(if: $boolVar) and @include(if: $boolVar) are actually good + // targets to extract. We may want to revisit when Data Connect adds more + // field-level directives. + directiveName = node.name.value; + }, + leave() { + directiveName = undefined; + }, + }, + Field: { + enter(node) { + fieldPath.push((node.alias ?? node.name).value); + }, + leave() { + fieldPath.pop(); + }, + }, + Argument: { + enter(node) { + if (argName) { + // This should be impossible to reach. + throw new Error( + `Found Argument within Argument: (${argName} > ${node.name.value}).`, + ); + } + argName = node.name.value; + const arg = typeInfo.getArgument(); + if (!arg) { + throw new Error( + `Cannot resolve argument type for ${displayPath( + fieldPath, + directiveName, + argName, + )}.`, + ); + } + if (addCandidate(node, arg.type)) { + argName = undefined; + return false; // Skip extracting parts of this argument. + } + }, + leave() { + argName = undefined; + }, + }, + ObjectField: { + enter(node) { + valuePath.push(node.name.value); + const input = typeInfo.getInputType(); + if (!input) { + // This may happen if a scalar (such as JSON) type has a value of + // a nested structure (objects / lists). We cannot infer the + // actual required "type" of the sub-structure in this case. + return false; + } + if (addCandidate(node, input)) { + valuePath.pop(); + return false; // Skip extracting fields within this object. + } + }, + leave() { + valuePath.pop(); + }, + }, + ListValue: { + enter() { + // We don't know how to extract repeated variables yet. + // Exception: A key scalar may be extracted as a whole even if its + // value is in array format. Those cases are handled by the scalar + // checks in Argument and ObjectField and should never reach here. + return false; + }, + }, + }), + ); + return candidates; + + function addCandidate( + node: ObjectFieldNode | ArgumentNode, + type: GraphQLInputType, + ): boolean { + if (!isConstValueNode(node.value)) { + return false; + } + if (!isExtractableType(type)) { + return false; + } + const varName = suggestVarName( + seenVarNames, + fieldPath, + directiveName, + argName, + valuePath, + ); + seenVarNames.add(varName); + candidates.push({ + defaultValue: node.value, + parentNode: node, + varName, + type, + label: "$" + varName, + description: `: ${type} = ${print(node.value)}`, + detail: displayPath( + fieldPath, + directiveName, + argName, + valuePath, + "$" + varName, + ), + // Typical enums such as OrderBy are unlikely to be made variables. + // Similarly, null literals aren't usually meant to be changed. + picked: !isEnumType(type) && node.value.kind !== Kind.NULL, + }); + return true; + } + } + + function extractVariables( + opAst: DocumentNode, + picked: ExtractCandidate[], + ): DocumentNode { + const pickedByParent = new Map(); + for (const p of picked) { + pickedByParent.set(p.parentNode, p); + } + + return visit(opAst, { + enter(node) { + const extract = pickedByParent.get(node); + if (extract) { + const newVal: VariableNode = { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: extract.varName, + }, + }; + return { ...node, value: newVal }; + } + }, + OperationDefinition: { + leave(node) { + const variableDefinitions = [...node.variableDefinitions]; + for (const extract of picked) { + variableDefinitions.push({ + kind: Kind.VARIABLE_DEFINITION, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: extract.varName, + }, + }, + defaultValue: + extract.defaultValue.kind === Kind.NULL + ? undefined // Omit `= null`. + : extract.defaultValue, + type: toTypeNode(extract.type), + }); + } + const directives = [...node.directives]; + directives.push({ + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: "auth", + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: "level", + }, + value: { + kind: Kind.ENUM, + value: "PUBLIC", + }, + }, + ], + }); + return { ...node, variableDefinitions, directives }; + }, + }, + }); + } + + function displayPath( + fieldPath: string[], + directiveName?: string, + argName?: string, + valuePath?: string[], + valueDisp = "", + ): string { + let fieldDisp = fieldPath.join("."); + if (directiveName) { + fieldDisp += ` @${directiveName}`; + } + if (!argName) { + return fieldDisp; + } + if (valuePath) { + // or {foo: } or {parent: {foo: }} or so on. + for (let i = valuePath.length - 1; i >= 0; i--) { + valueDisp = `{${valuePath[i]}: ${valueDisp}}`; + } + valueDisp = " " + valueDisp; + } else { + valueDisp = ""; + } + return fieldDisp + `(${argName}:${valueDisp})`; + } + + function suggestVarName( + seenVarNames: Set, + fieldPath: string[], + directiveName?: string, + argName?: string, + valuePath?: string[], + ): string { + const path = [...fieldPath]; + if (argName) { + path.push(argName); + } + if (directiveName) { + path.push(directiveName); + } + if (valuePath) { + path.push(...valuePath); + } + // Consider all path segments (starting from the local name) and keep adding + // more prefixes or numbers. e.g., for `foo_insert(data: {id: })`: + // $id => $dataId => $fooInsertDataId => $fooInsertDataId2, in that order. + let varName = path[path.length - 1]; + for (let i = path.length - 2; i >= 0; i--) { + if (seenVarNames.has(varName)) { + varName = camelCase(`${path[i]}-${varName}`); + } + } + if (seenVarNames.has(varName)) { + for (let i = 2; i < 100; i++) { + if (!seenVarNames.has(varName + i.toString())) { + varName += i.toString(); + break; + } + } + // In the extremely rare case, we may reach here and the variable name + // may be already taken and we'll let the developer resolve this problem. + } + return varName; + } + + return Disposable.from( + vscode.commands.registerCommand( + "firebase.dataConnect.moveOperationToConnector", + (number, location, connectorPath) => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.MOVE_TO_CONNECTOR); + moveOperationToConnector(number, location, connectorPath); + }, + ), + ); +} + +interface ExtractCandidate extends vscode.QuickPickItem { + defaultValue: ConstValueNode; + parentNode: ArgumentNode | ObjectFieldNode; + varName: string; + type: ExtractableType; +} + +type ExtractableType = GraphQLLeafType | GraphQLNonNull; + +function isExtractableType(type: unknown): type is ExtractableType { + if (isNonNullType(type)) { + type = type.ofType; + } + if (isLeafType(type)) { + return true; + } + return false; +} + +function toTypeNode(type: ExtractableType): TypeNode { + if (isNonNullType(type)) { + return { + kind: Kind.NON_NULL_TYPE, + type: toNamedTypeNode(type.ofType), + }; + } + return toNamedTypeNode(type); +} + +function toNamedTypeNode(type: GraphQLLeafType): NamedTypeNode { + return { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: type.name, + }, + }; +} diff --git a/firebase-vscode/src/data-connect/core-compiler.ts b/firebase-vscode/src/data-connect/core-compiler.ts new file mode 100644 index 00000000000..1379526452a --- /dev/null +++ b/firebase-vscode/src/data-connect/core-compiler.ts @@ -0,0 +1,140 @@ +import * as vscode from "vscode"; +import { Range, DiagnosticSeverity, Diagnostic, Uri, Position } from "vscode"; +import fetch from "node-fetch"; +import { GraphQLError } from "graphql"; +import { Observable, of } from "rxjs"; +import { backOff } from "exponential-backoff"; +import { ResolvedDataConnectConfigs, dataConnectConfigs } from "./config"; +import { DataConnectConfig } from "../firebaseConfig"; + +type DiagnosticTuple = [Uri, Diagnostic[]]; +type CompilerResponse = { result?: { errors?: GraphQLError[] } }; + +const fdcDiagnosticCollection = + vscode.languages.createDiagnosticCollection("Dataconnect"); +/** + * + * @param fdcEndpoint FDC Emulator endpoint + */ +export async function runDataConnectCompiler( + configs: ResolvedDataConnectConfigs, + fdcEndpoint: string, +) { + const obsErrors = await getCompilerStream(configs, fdcEndpoint); + const obsConverter = { + next(nextCompilerResponse: CompilerResponse) { + if (nextCompilerResponse.result && nextCompilerResponse.result.errors) { + fdcDiagnosticCollection.clear(); + const diagnostics = convertGQLErrorToDiagnostic( + configs, + nextCompilerResponse.result.errors, + ); + fdcDiagnosticCollection.set(diagnostics); + } + }, + error(e: Error) { + console.log("Stream closed with: ", e); + }, + complete() { + console.log("Stream Closed"); + }, + }; + obsErrors.subscribe(obsConverter); +} + +function convertGQLErrorToDiagnostic( + configs: ResolvedDataConnectConfigs, + gqlErrors: GraphQLError[], +): DiagnosticTuple[] { + const perFileDiagnostics = {}; + const dcPath = configs.values[0].path; + for (const error of gqlErrors) { + const absFilePath = `${dcPath}/${error.extensions["file"]}`; + const perFileDiagnostic = perFileDiagnostics[absFilePath] || []; + perFileDiagnostic.push({ + source: "Firebase Data Connect: Compiler", + message: error.message, + severity: DiagnosticSeverity.Error, + range: locationToRange(error.locations[0]), + } as Diagnostic); + perFileDiagnostics[absFilePath] = perFileDiagnostic; + } + return Object.keys(perFileDiagnostics).map((key) => { + return [ + Uri.file(key), + perFileDiagnostics[key] as Diagnostic[], + ] as DiagnosticTuple; + }); +} + +// Basic conversion from GraphQLError.SourceLocation to Range +function locationToRange(location: { line: number; column: number }): Range { + const pos1 = new Position(location["line"] - 1, location["column"]); + const pos2 = new Position(location["line"] - 1, location["column"]); + return new Range(pos1, pos2); +} + +/** + * Calls the DataConnect.StreamCompileErrors api. + * Converts ReadableStream into Observable + * */ + +export async function getCompilerStream( + configs: ResolvedDataConnectConfigs, + dataConnectEndpoint: string, +): Promise> { + try { + // TODO: eventually support multiple services + const serviceId = configs.serviceIds[0]; + const resp = await backOff(() => + fetch( + dataConnectEndpoint + `/emulator/stream_errors?serviceId=${serviceId}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-mantle-admin": "all", + }, + }, + ), + ); + + function fromStream( + stream: NodeJS.ReadableStream, + finishEventName = "end", + dataEventName = "data", + ): Observable { + stream.pause(); + + return new Observable((observer) => { + function dataHandler(data: any) { + observer.next(JSON.parse(data)); + } + + function errorHandler(err: any) { + observer.error(JSON.parse(err)); + } + + function endHandler() { + observer.complete(); + } + + stream.addListener(dataEventName, dataHandler); + stream.addListener("error", errorHandler); + stream.addListener(finishEventName, endHandler); + + stream.resume(); + + return () => { + stream.removeListener(dataEventName, dataHandler); + stream.removeListener("error", errorHandler); + stream.removeListener(finishEventName, endHandler); + }; + }); + } + return fromStream(resp.body!); + } catch (err) { + console.log("Stream failed to connect with error: ", err); + return of({}); + } +} diff --git a/firebase-vscode/src/data-connect/deploy.ts b/firebase-vscode/src/data-connect/deploy.ts new file mode 100644 index 00000000000..907602dcc56 --- /dev/null +++ b/firebase-vscode/src/data-connect/deploy.ts @@ -0,0 +1,134 @@ +import * as vscode from "vscode"; +import { firstWhere, firstWhereDefined } from "../utils/signal"; +import { currentOptions } from "../options"; +import { deploy as cliDeploy } from "../../../src/deploy"; +import { dataConnectConfigs } from "./config"; +import { createE2eMockable } from "../utils/test_hooks"; +import { runCommand } from "./terminal"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; + +function createDeployOnlyCommand(serviceConnectorMap: { + [key: string]: string[]; +}): string { + return ( + "firebase deploy --only " + + Object.entries(serviceConnectorMap) + .map(([serviceId, connectorIds]) => { + return ( + `dataconnect:${serviceId}:schema,` + + connectorIds + .map((connectorId) => `dataconnect:${serviceId}:${connectorId}`) + .join(",") + ); + }) + .join(",") + ); +} + +export function registerFdcDeploy( + broker: ExtensionBrokerImpl, + telemetryLogger: vscode.TelemetryLogger, +): vscode.Disposable { + const deploySpy = createE2eMockable( + async (...args: Parameters) => { + // Have the "deploy" return "void" for easier mocking (no return value when spied). + cliDeploy(...args); + }, + "deploy", + async () => {}, + ); + + const deployAllCmd = vscode.commands.registerCommand("fdc.deploy-all", () => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.DEPLOY_ALL); + runCommand("firebase deploy --only dataconnect"); + }); + + const deployCmd = vscode.commands.registerCommand("fdc.deploy", async () => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.DEPLOY_INDIVIDUAL); + const configs = await firstWhereDefined(dataConnectConfigs).then( + (c) => c.requireValue, + ); + + const pickedServices = await pickServices(configs.serviceIds); + if (!pickedServices.length) { + return; + } + + const serviceConnectorMap: { [key: string]: string[] } = {}; + for (const serviceId of pickedServices) { + const connectorIds = configs.findById(serviceId)?.connectorIds; + serviceConnectorMap[serviceId] = await pickConnectors( + connectorIds, + serviceId, + ); + } + + runCommand(createDeployOnlyCommand(serviceConnectorMap)); // run from terminal + }); + + const deployAllSub = broker.on("fdc.deploy-all", async () => + vscode.commands.executeCommand("fdc.deploy-all"), + ); + + const deploySub = broker.on("fdc.deploy", async () => + vscode.commands.executeCommand("fdc.deploy"), + ); + + return vscode.Disposable.from( + deploySpy, + deployAllCmd, + deployCmd, + { dispose: deployAllSub }, + { dispose: deploySub }, + ); +} + +async function pickServices( + serviceIds: string[], +): Promise | undefined> { + const options = firstWhere( + currentOptions, + (options) => options.project?.length !== 0, + ).then((options) => { + return serviceIds.map((serviceId) => { + return { + label: serviceId, + options, + picked: true, + }; + }); + }); + + const picked = await vscode.window.showQuickPick(options, { + title: "Select services to deploy", + canPickMany: true, + }); + + return picked.filter((e) => e.picked).map((service) => service.label); +} + +async function pickConnectors( + connectorIds: string[] | undefined, + serviceId: string, +): Promise | undefined> { + const options = firstWhere( + currentOptions, + (options) => options.project?.length !== 0, + ).then((options) => { + return connectorIds?.map((connectorId) => { + return { + label: connectorId, + options, + picked: true, + }; + }); + }); + + const picked = await vscode.window.showQuickPick(options, { + title: `Select connectors to deploy for: ${serviceId}`, + canPickMany: true, + }); + + return picked.filter((e) => e.picked).map((connector) => connector.label); +} diff --git a/firebase-vscode/src/data-connect/emulator-stream.ts b/firebase-vscode/src/data-connect/emulator-stream.ts new file mode 100644 index 00000000000..840bf8ea106 --- /dev/null +++ b/firebase-vscode/src/data-connect/emulator-stream.ts @@ -0,0 +1,141 @@ +import * as vscode from "vscode"; +import fetch from "node-fetch"; +import { Observable, of } from "rxjs"; +import { backOff } from "exponential-backoff"; +import { ResolvedDataConnectConfigs } from "./config"; +import { Signal } from "@preact/signals-core"; + +enum Kind { + KIND_UNSPECIFIED = "KIND_UNSPECIFIED", + SQL_CONNECTION = "SQL_CONNECTION", + SQL_MIGRATION = "SQL_MIGRATION", + VERTEX_AI = "VERTEX_AI", +} +enum Severity { + SEVERITY_UNSPECIFIED = "SEVERITY_UNSPECIFIED", + DEBUG = "DEBUG", + NOTICE = "NOTICE", + ALERT = "ALERT", +} +interface EmulatorIssue { + kind: Kind; + severity: Severity; + message: string; +} + +type EmulatorIssueResponse = { result?: { issues?: EmulatorIssue[] } }; + +export const emulatorOutputChannel = + vscode.window.createOutputChannel("Firebase Emulators"); + +/** + * TODO: convert to class + * @param fdcEndpoint FDC Emulator endpoint + */ +export async function runEmulatorIssuesStream( + configs: ResolvedDataConnectConfigs, + fdcEndpoint: string, + isPostgresEnabled: Signal, +) { + const obsErrors = await getEmulatorIssuesStream(configs, fdcEndpoint); + const obsConverter = { + next(nextResponse: EmulatorIssueResponse) { + if (nextResponse.result?.issues?.length) { + for (const issue of nextResponse.result.issues) { + displayAndHandleIssue(issue, isPostgresEnabled); + } + } + }, + error(e: Error) { + console.log("Stream closed with: ", e); + }, + complete() { + console.log("Stream Closed"); + }, + }; + obsErrors.subscribe(obsConverter); +} + +/** + * Based on the severity of the issue, either log, display notification, or display interactive popup to the user + */ +export function displayAndHandleIssue(issue: EmulatorIssue, isPostgresEnabled: Signal) { + const issueMessage = `Data Connect Emulator: ${issue.kind.toString()} - ${issue.message}`; + if (issue.severity === Severity.ALERT) { + vscode.window.showErrorMessage(issueMessage); + } else if (issue.severity === Severity.NOTICE) { + vscode.window.showWarningMessage(issueMessage); + } + emulatorOutputChannel.appendLine(issueMessage); + + // special handlings + if (issue.kind === Kind.SQL_CONNECTION) { + isPostgresEnabled.value = false; + } +} + +/** + * Calls the DataConnect.StreamEmulatorIssues api. + * Converts ReadableStream into Observable + * + */ +export async function getEmulatorIssuesStream( + configs: ResolvedDataConnectConfigs, + dataConnectEndpoint: string, +): Promise> { + try { + // TODO: eventually support multiple services + const serviceId = configs.serviceIds[0]; + + const resp = await backOff(() => + fetch( + dataConnectEndpoint + `/emulator/stream_issues?serviceId=${serviceId}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-mantle-admin": "all", + }, + }, + ), + ); + + function fromStream( + stream: NodeJS.ReadableStream, + finishEventName = "end", + dataEventName = "data", + ): Observable { + stream.pause(); + + return new Observable((observer) => { + function dataHandler(data: any) { + observer.next(JSON.parse(data)); + } + + function errorHandler(err: any) { + observer.error(JSON.parse(err)); + } + + function endHandler() { + observer.complete(); + } + + stream.addListener(dataEventName, dataHandler); + stream.addListener("error", errorHandler); + stream.addListener(finishEventName, endHandler); + + stream.resume(); + + return () => { + stream.removeListener(dataEventName, dataHandler); + stream.removeListener("error", errorHandler); + stream.removeListener(finishEventName, endHandler); + }; + }); + } + return fromStream(resp.body!); + } catch (err) { + console.log("Stream failed to connect with error: ", err); + return of({}); + } +} diff --git a/firebase-vscode/src/data-connect/emulator.ts b/firebase-vscode/src/data-connect/emulator.ts new file mode 100644 index 00000000000..69f30a30406 --- /dev/null +++ b/firebase-vscode/src/data-connect/emulator.ts @@ -0,0 +1,168 @@ +import { EmulatorsController } from "../core/emulators"; +import * as vscode from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { effect, signal } from "@preact/signals-core"; +import { firstWhereDefined } from "../utils/signal"; +import { firebaseRC, updateFirebaseRCProject } from "../core/config"; +import { + DataConnectEmulatorClient, + dataConnectEmulatorEvents, +} from "../../../src/emulator/dataconnectEmulator"; +import { dataConnectConfigs } from "./config"; +import { runEmulatorIssuesStream } from "./emulator-stream"; +import { runDataConnectCompiler } from "./core-compiler"; +import { pluginLogger } from "../logger-wrapper"; +import { Emulators, getEmulatorDetails, listRunningEmulators } from "../cli"; +import { emulatorOutputChannel } from "./emulator-stream"; + +/** FDC-specific emulator logic */ +export class DataConnectEmulatorController implements vscode.Disposable { + constructor( + readonly emulatorsController: EmulatorsController, + readonly broker: ExtensionBrokerImpl, + ) { + function notifyIsConnectedToPostgres(isConnected: boolean) { + broker.send("notifyIsConnectedToPostgres", isConnected); + } + + // on emulator restart, re-connect + dataConnectEmulatorEvents.on("restart", () => { + pluginLogger.log("Emulator started"); + this.isPostgresEnabled.value = false; + this.connectToEmulator(); + }); + + this.subs.push( + broker.on("connectToPostgres", () => this.connectToPostgres()), + + // Notify webviews when the emulator status changes + effect(() => { + if (this.isPostgresEnabled.value) { + this.emulatorsController.emulatorStatusItem.show(); + } else { + this.emulatorsController.emulatorStatusItem.hide(); + } + notifyIsConnectedToPostgres(this.isPostgresEnabled.value); + }), + + // Notify the webview of the initial state + broker.on("getInitialIsConnectedToPostgres", () => { + notifyIsConnectedToPostgres(this.isPostgresEnabled.value); + }), + ); + } + + readonly isPostgresEnabled = signal(false); + private readonly subs: Array<() => void> = []; + + private async promptConnectionString( + defaultConnectionString: string, + ): Promise { + const connectionString = await vscode.window.showInputBox({ + title: "Enter a Postgres connection string", + prompt: + "A Postgres database must be configured to use the emulator locally.", + value: defaultConnectionString, + }); + + return connectionString; + } + + private async connectToPostgres() { + const rc = firebaseRC.value?.tryReadValue; + let localConnectionString = + rc?.getDataconnect()?.postgres?.localConnectionString; + if (!localConnectionString) { + const dataConnectConfigsValue = + await firstWhereDefined(dataConnectConfigs); + let dbname = "postgres"; + const postgresql = + dataConnectConfigsValue?.tryReadValue.values[0]?.value?.schema + ?.datasource?.postgresql; + if (postgresql) { + const instanceId = postgresql.cloudSql?.instanceId; + const databaseName = postgresql.database; + if (instanceId && databaseName) { + dbname = `${instanceId}-${databaseName}`; + } + } + localConnectionString = `postgres://user:password@localhost:5432/${dbname}`; + } + const newConnectionString = await this.promptConnectionString( + localConnectionString, + ); + if (!newConnectionString) { + return; + } + + // notify sidebar webview of connection string + this.broker.send("notifyPostgresStringChanged", newConnectionString); + + updateFirebaseRCProject({ + fdcPostgresConnectionString: newConnectionString, + }); + + // configure the emulator to use the local psql string + const emulatorClient = new DataConnectEmulatorClient(); + this.isPostgresEnabled.value = true; + + emulatorClient.configureEmulator({ connectionString: newConnectionString }); + } + + // on schema reload, restart language server and run introspection again + private async schemaReload() { + vscode.commands.executeCommand("fdc-graphql.restart"); + vscode.commands.executeCommand( + "firebase.dataConnect.executeIntrospection", + ); + } + + // Commands to run after the emulator is started successfully + private async connectToEmulator() { + this.schemaReload(); + const configs = dataConnectConfigs.value?.tryReadValue; + + runEmulatorIssuesStream( + configs, + this.emulatorsController.getLocalEndpoint().value, + this.isPostgresEnabled, + ); + runDataConnectCompiler( + configs, + this.emulatorsController.getLocalEndpoint().value, + ); + + this.setupOutputChannel(); + } + + private setupOutputChannel() { + if ( + listRunningEmulators().filter((emulatorInfos) => { + emulatorInfos.name === Emulators.DATACONNECT; + }) + ) { + const dataConnectEmulatorDetails = getEmulatorDetails( + Emulators.DATACONNECT, + ); + + // Child process is only available when emulator is started through vscode + if (dataConnectEmulatorDetails.instance) { + dataConnectEmulatorDetails.instance.stdout?.on("data", (data) => { + emulatorOutputChannel.appendLine("DEBUG: " + data.toString()); + }); + // TODO: Utilize streaming api to iniate schema reloads + dataConnectEmulatorDetails.instance.stderr?.on("data", (data) => { + if (data.toString().includes("Finished reloading")) { + this.schemaReload(); + } else { + emulatorOutputChannel.appendLine("ERROR: " + data.toString()); + } + }); + } + } + } + + dispose() { + this.subs.forEach((sub) => sub()); + } +} diff --git a/firebase-vscode/src/data-connect/execution-history-provider.ts b/firebase-vscode/src/data-connect/execution-history-provider.ts new file mode 100644 index 00000000000..96cd6cdda79 --- /dev/null +++ b/firebase-vscode/src/data-connect/execution-history-provider.ts @@ -0,0 +1,96 @@ +import * as vscode from "vscode"; // from //third_party/vscode/src/vs:vscode +import { effect } from "@preact/signals-core"; +import { ExecutionItem, ExecutionState, executions } from "./execution-store"; + +const timeFormatter = new Intl.DateTimeFormat("default", { + timeStyle: "long", +}); + +/** + * The TreeItem for an execution. + */ +export class ExecutionTreeItem extends vscode.TreeItem { + parent?: ExecutionTreeItem; + children: ExecutionTreeItem[] = []; + + constructor(readonly item: ExecutionItem) { + super(item.label, vscode.TreeItemCollapsibleState.None); + this.item = item; + + // Renders arguments in a single line + const prettyArgs = this.item.args?.replaceAll(/[\n \t]+/g, " "); + this.description = `${timeFormatter.format( + item.timestamp + )} | Arguments: ${prettyArgs}`; + this.command = { + title: "Show result", + command: "firebase.dataConnect.selectExecutionResultToShow", + arguments: [item.executionId], + }; + this.updateContext(); + } + + updateContext() { + this.contextValue = "executionTreeItem-finished"; + if (this.item.state === ExecutionState.FINISHED) { + this.iconPath = new vscode.ThemeIcon( + "pass", + new vscode.ThemeColor("testing.iconPassed") + ); + } else if (this.item.state === ExecutionState.CANCELLED) { + this.iconPath = new vscode.ThemeIcon( + "warning", + new vscode.ThemeColor("testing.iconErrored") + ); + } else if (this.item.state === ExecutionState.ERRORED) { + this.iconPath = new vscode.ThemeIcon( + "close", + new vscode.ThemeColor("testing.iconFailed") + ); + } else if (this.item.state === ExecutionState.RUNNING) { + this.contextValue = "executionTreeItem-running"; + this.iconPath = new vscode.ThemeIcon( + "sync~spin", + new vscode.ThemeColor("testing.runAction") + ); + } + } +} + +/** + * The TreeDataProvider for data connect execution history. + */ +export class ExecutionHistoryTreeDataProvider + implements vscode.TreeDataProvider +{ + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this.onDidChangeTreeDataEmitter.event; + executionItems: ExecutionTreeItem[] = []; + + constructor() { + effect(() => { + this.executionItems = Object.values(executions.value) + .sort((a, b) => b.timestamp - a.timestamp) + .map((item) => new ExecutionTreeItem(item)); + + this.onDidChangeTreeDataEmitter.fire(); + }); + } + + getTreeItem(element: ExecutionTreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: ExecutionTreeItem): ExecutionTreeItem[] { + if (element) { + return element.children; + } else { + return this.executionItems; + } + } + + getParent(element?: ExecutionTreeItem): ExecutionTreeItem | undefined { + return element?.parent; + } +} diff --git a/firebase-vscode/src/data-connect/execution-store.ts b/firebase-vscode/src/data-connect/execution-store.ts new file mode 100644 index 00000000000..38f8c6c9eb1 --- /dev/null +++ b/firebase-vscode/src/data-connect/execution-store.ts @@ -0,0 +1,83 @@ +import { computed } from "@preact/signals-core"; +import { ExecutionResult, OperationDefinitionNode } from "graphql"; +import * as vscode from "vscode"; +import { globalSignal } from "../utils/globals"; + +export enum ExecutionState { + INIT, + RUNNING, + CANCELLED, + ERRORED, + FINISHED, +} + +export interface ExecutionItem { + executionId: string; + label: string; + timestamp: number; + state: ExecutionState; + operation: OperationDefinitionNode; + args?: string; + results?: ExecutionResult | Error; + documentPath: string; + position: vscode.Position; +} + +let executionId = 0; + +function nextExecutionId() { + executionId++; + return `${executionId}`; +} + +export const executions = globalSignal< + Record +>({}); + +export const selectedExecutionId = globalSignal(""); + +/** The unparsed JSON object mutation/query variables. + * + * The JSON may be invalid. + */ +export const executionArgsJSON = globalSignal("{}"); + +export function createExecution( + executionItem: Omit +) { + const item: ExecutionItem = { + executionId: nextExecutionId(), + ...executionItem, + }; + + executions.value = { + ...executions.value, + [executionId]: item, + }; + + return item; +} + +export function updateExecution( + executionId: string, + executionItem: ExecutionItem +) { + executions.value = { + ...executions.value, + [executionId]: executionItem, + }; +} + +export async function selectExecutionId(executionId: string) { + selectedExecutionId.value = executionId; + + // take user to operation location in editor + const { documentPath, position } = selectedExecution.value; + await vscode.window.showTextDocument(vscode.Uri.file(documentPath), { + selection: new vscode.Range(position, position), + }); +} + +export const selectedExecution = computed( + () => executions.value[selectedExecutionId.value] +); diff --git a/firebase-vscode/src/data-connect/execution.ts b/firebase-vscode/src/data-connect/execution.ts new file mode 100644 index 00000000000..bc03830e6a0 --- /dev/null +++ b/firebase-vscode/src/data-connect/execution.ts @@ -0,0 +1,242 @@ +import vscode, { + ConfigurationTarget, + Disposable, + ExtensionContext, + TelemetryLogger, +} from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { registerWebview } from "../webview"; +import { ExecutionHistoryTreeDataProvider } from "./execution-history-provider"; +import { + ExecutionItem, + ExecutionState, + createExecution, + executionArgsJSON, + selectExecutionId, + selectedExecution, + selectedExecutionId, + updateExecution, +} from "./execution-store"; +import { batch, effect } from "@preact/signals-core"; +import { OperationDefinitionNode, OperationTypeNode, print } from "graphql"; +import { DataConnectService } from "./service"; +import { DataConnectError, toSerializedError } from "../../common/error"; +import { OperationLocation } from "./types"; +import { EmulatorsController } from "../core/emulators"; +import { InstanceType } from "./code-lens-provider"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; + +export function registerExecution( + context: ExtensionContext, + broker: ExtensionBrokerImpl, + dataConnectService: DataConnectService, + emulatorsController: EmulatorsController, + telemetryLogger: TelemetryLogger, +): Disposable { + const treeDataProvider = new ExecutionHistoryTreeDataProvider(); + const executionHistoryTreeView = vscode.window.createTreeView( + "data-connect-execution-history", + { + treeDataProvider, + }, + ); + + // Select the corresponding tree-item when the selected-execution-id updates + const sub1 = effect(() => { + const id = selectedExecutionId.value; + const selectedItem = treeDataProvider.executionItems.find( + ({ item }) => item.executionId === id, + ); + executionHistoryTreeView.reveal(selectedItem, { select: true }); + }); + + function notifyDataConnectResults(item: ExecutionItem) { + broker.send("notifyDataConnectResults", { + args: item.args ?? "{}", + query: print(item.operation), + results: + item.results instanceof Error + ? toSerializedError(item.results) + : item.results, + displayName: item.operation.operation, + }); + } + + // Listen for changes to the selected-execution item + const sub2 = effect(() => { + const item = selectedExecution.value; + if (item) { + notifyDataConnectResults(item); + } + }); + + const sub3 = broker.on("getDataConnectResults", () => { + const item = selectedExecution.value; + if (item) { + notifyDataConnectResults(item); + } + }); + + async function executeOperation( + ast: OperationDefinitionNode, + { document, documentPath, position }: OperationLocation, + instance: InstanceType, + ) { + const configs = vscode.workspace.getConfiguration("firebase.dataConnect"); + const alwaysExecuteMutationsInProduction = + "alwaysAllowMutationsInProduction"; + const alwaysStartEmulator = "alwaysStartEmulator"; + + if ( + instance === InstanceType.LOCAL && + !emulatorsController.areEmulatorsRunning.value + ) { + const always = "Yes (always)"; + const yes = "Yes"; + const result = await vscode.window.showWarningMessage( + "Trying to execute an operation on the emulator, but it isn't started yet. " + + "Do you wish to start it?", + { modal: true }, + yes, + always, + ); + + // If the user selects "always", we update User settings. + if (result === always) { + configs.update(alwaysStartEmulator, true, ConfigurationTarget.Global); + } + + if (result === yes || result === always) { + telemetryLogger.logUsage( + DATA_CONNECT_EVENT_NAME.START_EMULATOR_FROM_EXECUTION, + ); + await vscode.commands.executeCommand("firebase.emulators.start"); + } else { + telemetryLogger.logUsage( + DATA_CONNECT_EVENT_NAME.REFUSE_START_EMULATOR_FROM_EXECUTION, + ); + } + } + + // Warn against using mutations in production. + if ( + instance !== InstanceType.LOCAL && + !configs.get(alwaysExecuteMutationsInProduction) && + ast.operation === OperationTypeNode.MUTATION + ) { + const always = "Yes (always)"; + const yes = "Yes"; + const result = await vscode.window.showWarningMessage( + "You are about to perform a mutation in production environment. Are you sure?", + { modal: true }, + yes, + always, + ); + + if (result !== always && result !== yes) { + return; + } + + // If the user selects "always", we update User settings. + if (result === always) { + configs.update( + alwaysExecuteMutationsInProduction, + true, + ConfigurationTarget.Global, + ); + } + } + + const item = createExecution({ + label: ast.name?.value ?? "anonymous", + timestamp: Date.now(), + state: ExecutionState.RUNNING, + operation: ast, + args: executionArgsJSON.value, + documentPath, + position, + }); + + function updateAndSelect(updates: Partial) { + batch(() => { + updateExecution(item.executionId, { ...item, ...updates }); + selectExecutionId(item.executionId); + }); + } + + try { + // Execute queries/mutations from their source code. + // That ensures that we can execute queries in unsaved files. + + const results = await dataConnectService.executeGraphQL({ + operationName: ast.name?.value, + // We send the whole unparsed document to guarantee + // that there are no formatting differences between the real document + // and the document that is sent to the emulator. + query: document, + variables: executionArgsJSON.value, + path: documentPath, + instance, + }); + + updateAndSelect({ + state: + // Executing queries may return a response which contains errors + // without throwing. + // In that case, we mark the execution as errored. + (results.errors?.length ?? 0) > 0 + ? ExecutionState.ERRORED + : ExecutionState.FINISHED, + results, + }); + } catch (error) { + updateAndSelect({ + state: ExecutionState.ERRORED, + results: + error instanceof Error + ? error + : new DataConnectError("Unknown error", error), + }); + } + } + + const sub4 = broker.on( + "definedDataConnectArgs", + (value) => (executionArgsJSON.value = value), + ); + + return Disposable.from( + { dispose: sub1 }, + { dispose: sub2 }, + { dispose: sub3 }, + { dispose: sub4 }, + registerWebview({ + name: "data-connect-execution-configuration", + context, + broker, + }), + registerWebview({ + name: "data-connect-execution-results", + context, + broker, + }), + executionHistoryTreeView, + vscode.commands.registerCommand( + "firebase.dataConnect.executeOperation", + (ast, location, instanceType: InstanceType) => { + telemetryLogger.logUsage( + instanceType === InstanceType.LOCAL + ? DATA_CONNECT_EVENT_NAME.RUN_LOCAL + : DATA_CONNECT_EVENT_NAME.RUN_PROD, + ); + executeOperation(ast, location, instanceType); + }, + ), + vscode.commands.registerCommand( + "firebase.dataConnect.selectExecutionResultToShow", + (executionId) => { + selectExecutionId(executionId); + }, + ), + ); +} diff --git a/firebase-vscode/src/data-connect/explorer-provider.ts b/firebase-vscode/src/data-connect/explorer-provider.ts new file mode 100644 index 00000000000..0eabe0cfffb --- /dev/null +++ b/firebase-vscode/src/data-connect/explorer-provider.ts @@ -0,0 +1,232 @@ +import * as vscode from "vscode"; // from //third_party/vscode/src/vs:vscode +import { CancellationToken, ExtensionContext } from "vscode"; + +import { + IntrospectionQuery, + IntrospectionType, + IntrospectionOutputType, + IntrospectionNamedTypeRef, + IntrospectionOutputTypeRef, + IntrospectionField, + TypeKind, +} from "graphql"; +import { effect } from "@preact/signals-core"; +import { introspectionQuery } from "./explorer"; +import { OPERATION_TYPE } from "./types"; + +interface Element { + name: string; + baseType: OPERATION_TYPE; +} + +export class ExplorerTreeDataProvider + implements vscode.TreeDataProvider +{ + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private typeSystem: + | { + introspection: IntrospectionQuery; + typeForName: Map; + } + | undefined = undefined; + + constructor() { + // on introspection change, update typesystem + effect(() => { + const introspection = introspectionQuery.value; + if (introspection) { + const typeForName = new Map(); + for (const type of introspection.__schema.types) { + typeForName.set(type.name, type); + } + this.typeSystem = { + introspection, + typeForName, + }; + this.refresh(); + } + }); + } + + refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + // sort by whether the element has children, so that list items show up last + private eleSortFn = (a: Element, b: Element) => { + const a_field = this._field(a); + const b_field = this._field(b); + const isAList = a_field.type.kind === TypeKind.OBJECT; + const isBList = b_field.type.kind === TypeKind.OBJECT; + if ((isAList && isBList) || (!isAList && !isBList)) { + return 0; + } else if (isAList) { + return 1; + } else { + return -1; + } + }; + + getTreeItem(element: Element): vscode.TreeItem { + // special cases for query and mutation root folders + if ( + Object.values(OPERATION_TYPE).includes(element.name as OPERATION_TYPE) + ) { + return new vscode.TreeItem( + element.name, + vscode.TreeItemCollapsibleState.Collapsed + ); + } + + const field = this._field(element); + if (!field) { + return undefined; + } + + const hasChildren = this._baseType(field).kind === TypeKind.OBJECT; + const label = field.name; + const treeItem = new vscode.TreeItem( + label, + hasChildren + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ); + + treeItem.description = this._formatType(field.type); + return treeItem; + } + + getChildren(element?: Element): Element[] { + // if the backend did not load yet + if (!introspectionQuery.value) { + return null; + } + // init the tree with two elements, query and mutation + if (!element) { + return [ + { name: OPERATION_TYPE.query, baseType: OPERATION_TYPE.query }, + { name: OPERATION_TYPE.mutation, baseType: OPERATION_TYPE.mutation }, + ]; + } + + if (element.name === OPERATION_TYPE.query) { + return this._unref(this.typeSystem.introspection.__schema.queryType) + .fields.filter((f) => f.name !== "_firebase") + .map((f) => { + return { name: f.name, baseType: OPERATION_TYPE.query }; + }); + } else if (element.name === OPERATION_TYPE.mutation) { + return this._unref(this.typeSystem.introspection.__schema.mutationType) + .fields.filter((f) => f.name !== "_firebase") + .map((f) => { + return { name: f.name, baseType: OPERATION_TYPE.mutation }; + }); + } + + const unwrapped = this._baseType(this._field(element)); + const type = this._unref(unwrapped); + if (type.kind === TypeKind.OBJECT) { + return type.fields + .map((field) => { + return { + name: `${element.name}.${field.name}`, + baseType: element.baseType, + }; + }) + .sort(this.eleSortFn); + } + return []; + } + + getParent(element: Element): vscode.ProviderResult { + const lastDot = element.name.indexOf("."); + if (lastDot <= 0) { + return undefined; + } + return { + name: element.name.substring(0, lastDot), + baseType: element.baseType, + }; + } + + resolveTreeItem( + item: vscode.TreeItem, + element: Element, + token: CancellationToken + ): vscode.ProviderResult { + const field = this._field(element); + item.tooltip = + field && field.description + ? new vscode.MarkdownString(field.description) + : ""; + + return item; + } + + private _field(element: Element): IntrospectionField | undefined { + const path = element.name.split("."); + const typeRef = + element.baseType === OPERATION_TYPE.query + ? this.typeSystem.introspection.__schema.queryType + : this.typeSystem.introspection.__schema.mutationType; + + if (!path.length) { + return undefined; + } + let field = undefined; + for (let i = 0; i < path.length; i++) { + const baseTypeRef = i === 0 ? typeRef : this._baseType(field); + + const type = this._unref(baseTypeRef); + if (type.kind !== TypeKind.OBJECT) { + return undefined; + } + const maybeField = type.fields.find((f) => f.name === path[i]); + if (!maybeField) { + return undefined; + } + field = maybeField; + } + return field; + } + + _unref(ref: IntrospectionNamedTypeRef): T { + const type = this.typeSystem.typeForName.get(ref.name); + if (!type) { + throw new Error( + `Introspection invariant violation: Ref type ${ref.name} does not exist` + ); + } + if (ref.kind && type.kind !== ref.kind) { + throw new Error( + `Introspection invariant violation: Ref kind ${ref.kind} does not match Type kind ${type.kind}` + ); + } + return type as T; + } + + _baseType( + field: IntrospectionField + ): IntrospectionNamedTypeRef { + let unwrapped = field.type; + while ( + unwrapped.kind === TypeKind.NON_NULL || + unwrapped.kind === TypeKind.LIST + ) { + unwrapped = unwrapped.ofType; + } + return unwrapped; + } + + _formatType(type: IntrospectionOutputTypeRef): string { + if (type.kind === TypeKind.NON_NULL) { + return this._formatType(type.ofType) + "!"; + } + if (type.kind === TypeKind.LIST) { + return `[${this._formatType(type.ofType)}]`; + } + return type.name; + } +} diff --git a/firebase-vscode/src/data-connect/explorer.ts b/firebase-vscode/src/data-connect/explorer.ts new file mode 100644 index 00000000000..823e28f262f --- /dev/null +++ b/firebase-vscode/src/data-connect/explorer.ts @@ -0,0 +1,38 @@ +import vscode, { Disposable, ExtensionContext } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { ExplorerTreeDataProvider } from "./explorer-provider"; +import { IntrospectionQuery } from "graphql"; +import { DataConnectService } from "./service"; +import { globalSignal } from "../utils/globals"; + +// explorer store +export const introspectionQuery = globalSignal( + undefined +); + +export function registerExplorer( + context: ExtensionContext, + broker: ExtensionBrokerImpl, + dataConnectService: DataConnectService +): Disposable { + const treeDataProvider = new ExplorerTreeDataProvider(); + const explorerTreeView = vscode.window.createTreeView( + "firebase.dataConnect.explorerView", + { + treeDataProvider, + } + ); + + async function executeIntrospection() { + const results = await dataConnectService.introspect(); + introspectionQuery.value = results.data; + } + + return Disposable.from( + explorerTreeView, + vscode.commands.registerCommand( + "firebase.dataConnect.executeIntrospection", + executeIntrospection + ) + ); +} diff --git a/firebase-vscode/src/data-connect/file-utils.ts b/firebase-vscode/src/data-connect/file-utils.ts new file mode 100644 index 00000000000..386873010da --- /dev/null +++ b/firebase-vscode/src/data-connect/file-utils.ts @@ -0,0 +1,42 @@ +import vscode, { Uri } from "vscode"; +import path from "path"; + +export async function checkIfFileExists(file: Uri) { + try { + await vscode.workspace.fs.stat(file); + return true; + } catch { + return false; + } +} + +export function isPathInside(childPath: string, parentPath: string): boolean { + const relative = path.relative(parentPath, childPath); + return !relative.startsWith("..") && !path.isAbsolute(relative); +} + +/** Opens a file in the editor. If the file is missing, opens an untitled file + * with the content provided by the `content` function. + */ +export async function upsertFile( + uri: vscode.Uri, + content: () => string | string, +): Promise { + const doesFileExist = await checkIfFileExists(uri); + + if (!doesFileExist) { + const doc = await vscode.workspace.openTextDocument( + uri.with({ scheme: "untitled" }), + ); + const editor = await vscode.window.showTextDocument(doc); + + await editor.edit((edit) => + edit.insert(new vscode.Position(0, 0), content()), + ); + return; + } + + // Opens existing text document + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc); +} diff --git a/firebase-vscode/src/data-connect/index.ts b/firebase-vscode/src/data-connect/index.ts new file mode 100644 index 00000000000..9c47be551e4 --- /dev/null +++ b/firebase-vscode/src/data-connect/index.ts @@ -0,0 +1,254 @@ +import vscode, { Disposable, ExtensionContext, TelemetryLogger } from "vscode"; +import { Signal, effect } from "@preact/signals-core"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { registerExecution } from "./execution"; +import { registerExplorer } from "./explorer"; +import { registerAdHoc } from "./ad-hoc-mutations"; +import { DataConnectService as FdcService } from "./service"; +import { + OperationCodeLensProvider, + SchemaCodeLensProvider, +} from "./code-lens-provider"; +import { registerConnectors } from "./connectors"; +import { AuthService } from "../auth/service"; +import { currentProjectId } from "../core/project"; +import { isTest } from "../utils/env"; +import { setupLanguageClient } from "./language-client"; +import { EmulatorsController } from "../core/emulators"; +import { registerFdcDeploy } from "./deploy"; +import * as graphql from "graphql"; +import { + ResolvedDataConnectConfigs, + dataConnectConfigs, + registerDataConnectConfigs, +} from "./config"; +import { locationToRange } from "../utils/graphql"; +import { Result } from "../result"; +import { LanguageClient } from "vscode-languageclient/node"; +import { registerTerminalTasks } from "./terminal"; +import { registerWebview } from "../webview"; + +import { DataConnectEmulatorController } from "./emulator"; + +class CodeActionsProvider implements vscode.CodeActionProvider { + constructor( + private configs: Signal< + Result | undefined + >, + ) {} + + provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + cancellationToken: vscode.CancellationToken, + ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + const documentText = document.getText(); + const results: (vscode.CodeAction | vscode.Command)[] = []; + + // TODO: replace w/ online-parser to work with malformed documents + const documentNode = graphql.parse(documentText); + let definitionAtRange: graphql.DefinitionNode | undefined; + let definitionIndex: number | undefined; + + for (let i = 0; i < documentNode.definitions.length; i++) { + const definition = documentNode.definitions[i]; + + if ( + definition.kind === graphql.Kind.OPERATION_DEFINITION && + definition.loc + ) { + const definitionRange = locationToRange(definition.loc); + const line = definition.loc.startToken.line - 1; + + if (!definitionRange.intersection(range)) { + continue; + } + + definitionAtRange = definition; + definitionIndex = i; + } + } + + if (!definitionAtRange) { + return null; + } + + this.moveToConnector( + document, + documentText, + { index: definitionIndex! }, + results, + ); + + return results; + } + + private moveToConnector( + document: vscode.TextDocument, + documentText: string, + { index }: { index: number }, + results: (vscode.CodeAction | vscode.Command)[], + ) { + const enclosingService = + this.configs.value?.tryReadValue?.findEnclosingServiceForPath( + document.uri.fsPath, + ); + if (!enclosingService) { + return; + } + + const enclosingConnector = enclosingService.findEnclosingConnectorForPath( + document.uri.fsPath, + ); + if (enclosingConnector) { + // Already in a connector, don't suggest moving to another one + return; + } + + for (const connectorResult of enclosingService.resolvedConnectors) { + const connector = connectorResult.tryReadValue; + if (!connector) { + // Parsing error in the connector, skip + continue; + } + + results.push({ + title: `Move to "${connector.value.connectorId}"`, + kind: vscode.CodeActionKind.Refactor, + tooltip: `Move to the connector to "${connector.path}"`, + command: "firebase.dataConnect.moveOperationToConnector", + arguments: [ + index, + { + document: documentText, + documentPath: document.fileName, + }, + connector.path, + ], + }); + } + } +} + +export function registerFdc( + context: ExtensionContext, + broker: ExtensionBrokerImpl, + authService: AuthService, + emulatorController: EmulatorsController, + telemetryLogger: TelemetryLogger, +): Disposable { + const fdcEmulatorsController = new DataConnectEmulatorController( + emulatorController, + broker, + ); + + const codeActions = vscode.languages.registerCodeActionsProvider( + [ + { scheme: "file", language: "graphql" }, + { scheme: "untitled", language: "graphql" }, + ], + new CodeActionsProvider(dataConnectConfigs), + { + providedCodeActionKinds: [vscode.CodeActionKind.Refactor], + }, + ); + + const fdcService = new FdcService(authService, emulatorController); + const operationCodeLensProvider = new OperationCodeLensProvider( + fdcEmulatorsController, + ); + const schemaCodeLensProvider = new SchemaCodeLensProvider(emulatorController); + + // activate language client/serer + let client: LanguageClient; + const lsOutputChannel: vscode.OutputChannel = + vscode.window.createOutputChannel("Firebase GraphQL Language Server"); + + // setup new language client on config change + context.subscriptions.push({ + dispose: effect(() => { + const configs = dataConnectConfigs.value?.tryReadValue; + if (client) { + client.stop(); + } + if (configs && configs.values.length > 0) { + client = setupLanguageClient(context, configs, lsOutputChannel); + vscode.commands.executeCommand("fdc-graphql.start"); + } + }), + }); + + const selectedProjectStatus = vscode.window.createStatusBarItem( + "projectPicker", + vscode.StatusBarAlignment.Left, + ); + selectedProjectStatus.tooltip = "Select a Firebase project"; + selectedProjectStatus.command = "firebase.selectProject"; + + const sub1 = effect(() => { + // Enable FDC views only if at least one dataconnect.yaml is present. + // TODO don't start the related logic unless a dataconnect.yaml is present + vscode.commands.executeCommand( + "setContext", + "firebase-vscode.fdc.enabled", + (dataConnectConfigs.value?.tryReadValue?.values.length ?? 0) !== 0, + ); + }); + + return Disposable.from( + fdcEmulatorsController, + codeActions, + selectedProjectStatus, + { dispose: sub1 }, + { + dispose: effect(() => { + selectedProjectStatus.text = `$(mono-firebase) ${ + currentProjectId.value ?? "" + }`; + selectedProjectStatus.show(); + }), + }, + registerDataConnectConfigs(broker), + registerExecution( + context, + broker, + fdcService, + emulatorController, + telemetryLogger, + ), + registerExplorer(context, broker, fdcService), + registerWebview({ name: "data-connect", context, broker }), + registerAdHoc(fdcService, telemetryLogger), + registerConnectors(context, broker, fdcService, telemetryLogger), + registerFdcDeploy(broker, telemetryLogger), + registerTerminalTasks(broker, telemetryLogger), + operationCodeLensProvider, + vscode.languages.registerCodeLensProvider( + // **Hack**: For testing purposes, enable code lenses on all graphql files + // inside the test_projects folder. + // This is because e2e tests start without graphQL installed, + // so code lenses would otherwise never show up. + isTest + ? [{ pattern: "/**/firebase-vscode/src/test/test_projects/**/*.gql" }] + : [ + { scheme: "file", language: "graphql" }, + { scheme: "untitled", language: "graphql" }, + ], + operationCodeLensProvider, + ), + schemaCodeLensProvider, + vscode.languages.registerCodeLensProvider( + [ + { scheme: "file", language: "graphql" }, + // Don't show in untitled files since the provider needs the file name. + ], + schemaCodeLensProvider, + ), + { + dispose: () => { + client.stop(); + }, + }, + ); +} diff --git a/firebase-vscode/src/data-connect/language-client.ts b/firebase-vscode/src/data-connect/language-client.ts new file mode 100644 index 00000000000..32882606257 --- /dev/null +++ b/firebase-vscode/src/data-connect/language-client.ts @@ -0,0 +1,143 @@ +import * as vscode from "vscode"; +import { + LanguageClientOptions, + ServerOptions, + TransportKind, + RevealOutputChannelOn, + LanguageClient, +} from "vscode-languageclient/node"; +import * as path from "node:path"; +import { ResolvedDataConnectConfigs } from "./config"; + +export function setupLanguageClient( + context, + configs: ResolvedDataConnectConfigs, + outputChannel: vscode.OutputChannel, +) { + const serverPath = path.join("dist", "server.js"); + const serverModule = context.asAbsolutePath(serverPath); + + const debugOptions = { + execArgv: ["--nolazy", "--inspect=localhost:6009"], + }; + + const serverOptions: ServerOptions = { + run: { + module: serverModule, + transport: TransportKind.ipc, + }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions, + }, + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: "file", language: "graphql" }], + synchronize: { + // TODO: This should include any referenced graphql files inside the graphql-config + fileEvents: [ + vscode.workspace.createFileSystemWatcher( + "/{graphql.config.*,.graphqlrc,.graphqlrc.*,package.json}", + false, + // Ignore change events for graphql config, we only care about create, delete and save events + // otherwise, the underlying language service is re-started on every key change. + // also, it makes sense that it should only re-load on file save, but we need to document that. + // TODO: perhaps we can intercept change events, and remind the user + // to save for the changes to take effect + true, + ), + // TODO: load ignore file + // These ignore node_modules and .git by default + vscode.workspace.createFileSystemWatcher( + "**/{*.graphql,*.graphqls,*.gql,*.js,*.mjs,*.cjs,*.esm,*.es,*.es6,*.jsx,*.ts,*.tsx,*.vue,*.svelte,*.cts,*.mts}", + ), + ], + }, + outputChannel, + outputChannelName: "GraphQL Language Server", + revealOutputChannelOn: RevealOutputChannelOn.Never, + initializationFailedHandler: (err) => { + outputChannel.appendLine("Initialization failed"); + outputChannel.appendLine(err.message); + if (err.stack) { + outputChannel.appendLine(err.stack); + } + outputChannel.show(); + return false; + }, + }; + + // Create the language client and start the client. + const client = new LanguageClient( + "graphQLlanguageServer", + "GraphQL Language Server", + serverOptions, + clientOptions, + ); + + // register commands + const commandShowOutputChannel = vscode.commands.registerCommand( + "fdc-graphql.showOutputChannel", + () => outputChannel.show(), + ); + + context.subscriptions.push(commandShowOutputChannel); + + const generateYamlFile = async () => { + const basePath = vscode.workspace.rootPath; + const filePath = ".firebase/.graphqlrc"; + const fileUri = vscode.Uri.file(`${basePath}/${filePath}`); + const folderPath = ".firebase"; + const folderUri = vscode.Uri.file(`${basePath}/${folderPath}`); + + // TODO: Expand to multiple services + const config = configs.values[0]; + const generatedPath = ".dataconnect"; + const schemaPaths = [ + `../${config.relativeSchemaPath}/**/*.gql`, + `../${config.relativePath}/${generatedPath}/**/*.gql`, + ]; + const documentPaths = config.relativeConnectorPaths.map( + (connectorPath) => `../${connectorPath}/**/*.gql`, + ); + + const yamlJson = JSON.stringify({ + schema: schemaPaths, + document: documentPaths, + }); + // create folder if needed + if (!vscode.workspace.getWorkspaceFolder(folderUri)) { + vscode.workspace.fs.createDirectory(folderUri); + } + vscode.workspace.fs.writeFile(fileUri, Buffer.from(yamlJson)); + }; + + vscode.commands.registerCommand("fdc-graphql.restart", async () => { + outputChannel.appendLine("Stopping Firebase GraphQL Language Server"); + await client.stop(); + await generateYamlFile(); + outputChannel.appendLine("Restarting Firebase GraphQL Language Server"); + await client.start(); + outputChannel.appendLine("Firebase GraphQL Language Server restarted"); + }); + + vscode.commands.registerCommand("fdc-graphql.start", async () => { + await generateYamlFile(); + await client.start(); + outputChannel.appendLine("Firebase GraphQL Language Server restarted"); + }); + + // ** DISABLED FOR NOW WHILE WE TEST GENERATED YAML ** + // restart server whenever config file changes + // const watcher = vscode.workspace.createFileSystemWatcher( + // "**/.graphqlrc.*", // TODO: extend to schema files, and other config types + // false, + // false, + // false, + // ); + // watcher.onDidChange(() => restartGraphqlLSP()); + + return client; +} diff --git a/firebase-vscode/src/data-connect/language-server.ts b/firebase-vscode/src/data-connect/language-server.ts new file mode 100644 index 00000000000..bd2c94e9b3d --- /dev/null +++ b/firebase-vscode/src/data-connect/language-server.ts @@ -0,0 +1,19 @@ +import { startServer } from "graphql-language-service-server"; +// The npm scripts are configured to only build this once before +// watching the extension, so please restart the extension debugger for changes! + +async function start() { + try { + await startServer({ + method: "node", + loadConfigOptions: { rootDir: ".firebase" }, + }); + // eslint-disable-next-line no-console + console.log("Firebase GraphQL Language Server started!"); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } +} + +void start(); diff --git a/firebase-vscode/src/data-connect/service.ts b/firebase-vscode/src/data-connect/service.ts new file mode 100644 index 00000000000..9da5c9f65d0 --- /dev/null +++ b/firebase-vscode/src/data-connect/service.ts @@ -0,0 +1,288 @@ +import fetch, { Response } from "node-fetch"; +import { + ExecutionResult, + IntrospectionQuery, + getIntrospectionQuery, +} from "graphql"; +import { computed } from "@preact/signals-core"; +import { assertExecutionResult } from "../../common/graphql"; +import { DataConnectError } from "../../common/error"; +import { AuthService } from "../auth/service"; +import { UserMockKind } from "../../common/messaging/protocol"; +import { firstWhereDefined } from "../utils/signal"; +import { EmulatorsController } from "../core/emulators"; +import { Emulators } from "../cli"; +import { dataConnectConfigs } from "../data-connect/config"; + +import { firebaseRC } from "../core/config"; +import { executeGraphQL } from "../../../src/dataconnect/dataplaneClient"; +import { + ExecuteGraphqlRequest, + ExecuteGraphqlResponse, + ExecuteGraphqlResponseError, + Impersonation, +} from "../dataconnect/types"; +import { ClientResponse } from "../apiv2"; +import { InstanceType } from "./code-lens-provider"; +import { pluginLogger } from "../logger-wrapper"; + +/** + * DataConnect Emulator service + */ +export class DataConnectService { + constructor( + private authService: AuthService, + private emulatorsController: EmulatorsController, + ) {} + + async servicePath( + path: string, + instance: InstanceType, + ): Promise { + const dataConnectConfigsValue = await firstWhereDefined(dataConnectConfigs); + // TODO: avoid calling this here and in getApiServicePathByPath + const serviceId = + dataConnectConfigsValue?.tryReadValue.findEnclosingServiceForPath(path) + .value.serviceId; + const projectId = firebaseRC.value?.tryReadValue?.projects?.default; + + if (serviceId === undefined || projectId === undefined) { + return undefined; + } + + return instance === InstanceType.PRODUCTION + ? dataConnectConfigsValue?.tryReadValue?.getApiServicePathByPath( + projectId, + path, + ) + : `projects/p/locations/l/services/${serviceId}`; + } + + private async decodeResponse( + response: Response, + format?: "application/json", + ): Promise { + const contentType = response.headers.get("Content-Type"); + if (!contentType) { + throw new Error("Invalid content type"); + } + + if (format && !contentType.includes(format)) { + throw new Error( + `Invalid content type. Expected ${format} but got ${contentType}`, + ); + } + + if (contentType.includes("application/json")) { + return response.json(); + } + + return response.text(); + } + private async handleProdResponse( + clientResponse: ClientResponse< + ExecuteGraphqlResponse | ExecuteGraphqlResponseError + >, + ): Promise { + if (!(clientResponse.status >= 200 && clientResponse.status < 300)) { + const errorResponse = + clientResponse as ClientResponse; + throw new DataConnectError( + `Request failed with status ${clientResponse.status}`, + errorResponse.body.error.message, + ); + } + const successResponse = + clientResponse as ClientResponse; + return successResponse.body; + } + + private async handleValidResponse( + response: Response, + ): Promise { + const json = await this.decodeResponse(response, "application/json"); + assertExecutionResult(json); + + return json; + } + + private async handleInvalidResponse(response: Response): Promise { + const cause = await this.decodeResponse(response); + + throw new DataConnectError( + `Request failed with status ${response.status}`, + cause, + ); + } + + private handleResponse(response: Response): Promise { + if (response.status >= 200 && response.status < 300) { + return this.handleValidResponse(response); + } + + return this.handleInvalidResponse(response); + } + + /** Encode a body while handling the fact that "variables" is raw JSON. + * + * If the JSON is invalid, will throw. + */ + private _serializeBody(body: { variables?: string; [key: string]: unknown }) { + if (!body.variables || body.variables.trim().length === 0) { + body.variables = undefined; + return JSON.stringify(body); + } + + // TODO: make this more efficient than a plain JSON decode+encode. + const { variables, ...rest } = body; + + return JSON.stringify({ + ...rest, + variables: JSON.parse(variables), + }); + } + + private _auth(): { impersonate?: Impersonation } { + const userMock = this.authService.userMock; + if (!userMock || userMock.kind === UserMockKind.ADMIN) { + return {}; + } + return { + impersonate: + userMock.kind === UserMockKind.AUTHENTICATED + ? { authClaims: JSON.parse(userMock.claims) } + : { unauthenticated: true }, + }; + } + + // This introspection is used to generate a basic graphql schema + // It will not include our predefined operations, which requires a DataConnect specific introspection query + async introspect(): Promise<{ data?: IntrospectionQuery }> { + try { + const introspectionResults = await this.executeGraphQLRead({ + query: getIntrospectionQuery(), + operationName: "IntrospectionQuery", + variables: "{}", + }); + console.log("introspection: ", introspectionResults); + // TODO: handle errors + if ((introspectionResults as any).errors.length > 0) { + return { data: undefined }; + } + // TODO: remove after core server handles this + for (let type of (introspectionResults as any).data.__schema.types) { + type.interfaces = []; + } + + return { data: (introspectionResults as any).data }; + } catch (e) { + // TODO: surface error that emulator is not connected + pluginLogger.error("error: ", e); + return { data: undefined }; + } + } + + async executeGraphQLRead(params: { + query: string; + operationName: string; + variables: string; + }) { + // TODO: get introspections for all services + const configs = await firstWhereDefined(dataConnectConfigs); + // Using "requireValue", so that if configs are not available, the execution should throw. + const serviceId = configs.requireValue.serviceIds[0]; + try { + // TODO: get name programmatically + const body = this._serializeBody({ + ...params, + name: `projects/p/locations/l/services/${serviceId}`, + extensions: {}, // Introspection is the only caller of executeGraphqlRead + }); + const resp = await fetch( + (await firstWhereDefined(this.emulatorsController.getLocalEndpoint())) + + `/v1alpha/projects/p/locations/l/services/${serviceId}:executeGraphqlRead`, + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-mantle-admin": "all", + }, + body, + }, + ); + const result = await resp.json().catch(() => resp.text()); + return result; + } catch (e) { + // TODO: actual error handling + pluginLogger.error(e); + return null; + } + } + + async executeGraphQL(params: { + query: string; + operationName?: string; + variables: string; + path: string; + instance: InstanceType; + }) { + const servicePath = await this.servicePath(params.path, params.instance); + if (!servicePath) { + throw new Error("No service found for path: " + params.path); + } + + const prodBody: ExecuteGraphqlRequest = { + operationName: params.operationName, + variables: JSON.parse(params.variables), + query: params.query, + name: `${servicePath}`, + extensions: this._auth(), + }; + + const body = this._serializeBody({ + ...params, + name: `${servicePath}`, + extensions: this._auth(), + }); + if (params.instance === InstanceType.PRODUCTION) { + const resp = await executeGraphQL(servicePath, prodBody); + return this.handleProdResponse(resp); + } else { + const resp = await fetch( + (await firstWhereDefined(this.emulatorsController.getLocalEndpoint())) + + `/v1alpha/${servicePath}:executeGraphql`, + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-mantle-admin": "all", + }, + body, + }, + ); + return this.handleResponse(resp); + } + } + + async connectToPostgres(connectionString: string): Promise { + try { + await fetch( + firstWhereDefined(this.emulatorsController.getLocalEndpoint()) + + `/emulator/configure?connectionString=${connectionString}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-mantle-admin": "all", + }, + }, + ); + return true; + } catch (e: any) { + pluginLogger.error(e); + return false; + } + } +} diff --git a/firebase-vscode/src/data-connect/terminal.ts b/firebase-vscode/src/data-connect/terminal.ts new file mode 100644 index 00000000000..a9129a1b5a2 --- /dev/null +++ b/firebase-vscode/src/data-connect/terminal.ts @@ -0,0 +1,79 @@ +import { TelemetryLogger, TerminalOptions } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import vscode, { Disposable } from "vscode"; +import { checkLogin } from "../core/user"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; +const environmentVariables = {}; + +const terminalOptions: TerminalOptions = { + name: "Data Connect Terminal", + env: environmentVariables, +}; + +export function setTerminalEnvVars(envVar: string, value: string) { + environmentVariables[envVar] = value; +} + +export function runCommand(command: string) { + const terminal = vscode.window.createTerminal(terminalOptions); + terminal.show(); + terminal.sendText(command); +} + +export function runTerminalTask( + taskName: string, + command: string, +): Promise { + const type = "firebase-" + Date.now(); + return new Promise(async (resolve, reject) => { + vscode.tasks.onDidEndTaskProcess(async (e) => { + if (e.execution.task.definition.type === type) { + e.execution.terminate(); + + if (e.exitCode === 0) { + resolve(`Successfully executed ${taskName} with command: ${command}`); + } else { + reject( + new Error(`Failed to execute ${taskName} with command: ${command}`), + ); + } + } + }); + vscode.tasks.executeTask( + new vscode.Task( + { type }, + vscode.TaskScope.Workspace, + taskName, + "firebase", + new vscode.ShellExecution(command), + ), + ); + }); +} + +export function registerTerminalTasks( + broker: ExtensionBrokerImpl, + telemetryLogger: TelemetryLogger, +): Disposable { + const loginTaskBroker = broker.on("executeLogin", () => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.IDX_LOGIN); + runTerminalTask("firebase login", "firebase login --no-localhost").then( + () => { + checkLogin(); + }, + ); + }); + + return Disposable.from( + { dispose: loginTaskBroker }, + vscode.commands.registerCommand( + "firebase.dataConnect.runTerminalTask", + (taskName, command) => { + telemetryLogger.logUsage(DATA_CONNECT_EVENT_NAME.COMMAND_EXECUTION, { + commandName: command, + }); + runTerminalTask(taskName, command); + }, + ), + ); +} diff --git a/firebase-vscode/src/data-connect/types.ts b/firebase-vscode/src/data-connect/types.ts new file mode 100644 index 00000000000..774b3320aaf --- /dev/null +++ b/firebase-vscode/src/data-connect/types.ts @@ -0,0 +1,12 @@ +import * as vscode from "vscode"; + +export enum OPERATION_TYPE { + query = "query", + mutation = "mutation", +} + +export interface OperationLocation { + document: string; + documentPath: string; + position: vscode.Position; +} diff --git a/firebase-vscode/src/extension-broker.ts b/firebase-vscode/src/extension-broker.ts new file mode 100644 index 00000000000..d323306b862 --- /dev/null +++ b/firebase-vscode/src/extension-broker.ts @@ -0,0 +1,46 @@ +import { Webview } from "vscode"; + +import { Broker, BrokerImpl } from "../common/messaging/broker"; +import { + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, +} from "../common/messaging/protocol"; +import { Message } from "../common/messaging/types"; + +export type ExtensionBrokerImpl = BrokerImpl< + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, + Webview +>; + +export class ExtensionBroker extends Broker< + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, + Webview +> { + private webviews: Webview[] = []; + + sendMessage( + command: string, + data: ExtensionToWebviewParamsMap[keyof ExtensionToWebviewParamsMap] + ): void { + for (const webview of this.webviews) { + webview.postMessage({ command, data }); + } + } + + registerReceiver(receiver: Webview) { + const webview = receiver; + this.webviews.push(webview); + webview.onDidReceiveMessage( + (message: Message) => { + this.executeListeners(message); + }, + null + ); + } + + delete(): void { + this.webviews = []; + } +} diff --git a/firebase-vscode/src/extension.ts b/firebase-vscode/src/extension.ts new file mode 100644 index 00000000000..042faf855f7 --- /dev/null +++ b/firebase-vscode/src/extension.ts @@ -0,0 +1,54 @@ +import * as vscode from "vscode"; + +import { ExtensionBroker } from "./extension-broker"; +import { createBroker } from "../common/messaging/broker"; +import { + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, +} from "../common/messaging/protocol"; +import { logSetup, pluginLogger } from "./logger-wrapper"; +import { registerWebview } from "./webview"; +import { registerCore } from "./core"; +import { getSettings } from "./utils/settings"; +import { registerFdc } from "./data-connect"; +import { AuthService } from "./auth/service"; +import { AnalyticsLogger } from "./analytics"; + +// This method is called when your extension is activated +export async function activate(context: vscode.ExtensionContext) { + const settings = getSettings(); + logSetup(settings); + pluginLogger.debug("Activating Firebase extension."); + + const broker = createBroker< + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, + vscode.Webview + >(new ExtensionBroker()); + + const authService = new AuthService(broker); + const analyticsLogger = new AnalyticsLogger(); + + const [emulatorsController, coreDisposable] = await registerCore( + broker, + context, + analyticsLogger.logger, + ); + + context.subscriptions.push( + coreDisposable, + registerWebview({ + name: "sidebar", + broker, + context, + }), + authService, + registerFdc( + context, + broker, + authService, + emulatorsController, + analyticsLogger.logger, + ), + ); +} diff --git a/firebase-vscode/src/logger-wrapper.ts b/firebase-vscode/src/logger-wrapper.ts new file mode 100644 index 00000000000..0e387f079be --- /dev/null +++ b/firebase-vscode/src/logger-wrapper.ts @@ -0,0 +1,97 @@ +import * as path from "path"; +import * as vscode from "vscode"; +import { transports, format } from "winston"; +import Transport from "winston-transport"; +import stripAnsi from "strip-ansi"; +import { SPLAT } from "triple-beam"; +import { logger as cliLogger } from "../../src/logger"; +import { setupLoggers, tryStringify } from "../../src/utils"; +import { setInquirerLogger } from "./stubs/inquirer-stub"; +import { getRootFolders } from "./core/config"; + +export type LogLevel = "debug" | "info" | "log" | "warn" | "error"; + +export const pluginLogger: Record void> = { + debug: () => {}, + info: () => {}, + log: () => {}, + warn: () => {}, + error: () => {}, +}; + +const outputChannel = vscode.window.createOutputChannel("Firebase"); + +export function showOutputChannel() { + outputChannel.show(); +} + +for (const logLevel in pluginLogger) { + pluginLogger[logLevel] = (...args) => { + const prefixedArgs = ["[Firebase Plugin]", ...args]; + cliLogger[logLevel](...prefixedArgs); + }; +} + +/** + * Logging setup for logging to console and to file. + */ +export function logSetup({ + shouldWriteDebug, + debugLogPath, +}: { + shouldWriteDebug: boolean; + debugLogPath: string; +}) { + // Log to console (use built in CLI functionality) + process.env.DEBUG = "true"; + setupLoggers(); + + // Log to file + // Only log to file if firebase.debug extension setting is true. + if (shouldWriteDebug) { + // Re-implement file logger call from ../../src/bin/firebase.ts to not bring + // in the entire firebase.ts file + const rootFolders = getRootFolders(); + const filePath = + debugLogPath || path.join(rootFolders[0], "firebase-plugin-debug.log"); + pluginLogger.info("Logging to path", filePath); + cliLogger.add( + new transports.File({ + level: "debug", + filename: filePath, + format: format.printf((info) => { + const segments = [info.message, ...(info[SPLAT] || [])].map( + tryStringify + ); + return `[${info.level}] ${stripAnsi(segments.join(" "))}`; + }), + }) + ); + cliLogger.add(new VSCodeOutputTransport({ level: "info" })); + } +} + +/** + * Custom Winston transport that writes to VSCode output channel. + * Write only "info" and greater to avoid too much spam from "debug". + */ +class VSCodeOutputTransport extends Transport { + constructor(opts) { + super(opts); + } + log(info, callback) { + setImmediate(() => { + this.emit("logged", info); + }); + const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify); + const text = `[${info.level}] ${stripAnsi(segments.join(" "))}`; + + if (info.level !== "debug") { + // info or greater: write to output window + outputChannel.appendLine(text); + } + + callback(); + } +} +setInquirerLogger(pluginLogger); diff --git a/firebase-vscode/src/metaprogramming.ts b/firebase-vscode/src/metaprogramming.ts new file mode 100644 index 00000000000..2cb166f8c30 --- /dev/null +++ b/firebase-vscode/src/metaprogramming.ts @@ -0,0 +1,5 @@ +export type DeepReadOnly = T extends Record + ? { readonly [K in keyof T]: DeepReadOnly } + : T extends Array + ? ReadonlyArray> + : T; diff --git a/firebase-vscode/src/options.ts b/firebase-vscode/src/options.ts new file mode 100644 index 00000000000..be8b937ec9f --- /dev/null +++ b/firebase-vscode/src/options.ts @@ -0,0 +1,125 @@ +import { RC } from "../../src/rc"; +import { Options } from "../../src/options"; +import { Command } from "../../src/command"; +import { ExtensionContext } from "vscode"; +import { setInquirerOptions } from "./stubs/inquirer-stub"; +import { Config } from "../../src/config"; +import { globalSignal } from "./utils/globals"; +import * as vscode from "vscode"; +import { effect } from "@preact/signals-core"; +import { firebaseConfig, firebaseRC, getConfigPath } from "./core/config"; + +export type VsCodeOptions = Options & { isVSCE: boolean; rc: RC | null }; + +const defaultOptions: Readonly = { + cwd: "", + configPath: "", + only: "", + except: "", + config: new Config({}), + filteredTargets: [], + force: true, + + // Options which are present on every command + project: "", + projectAlias: "", + projectId: "", + projectNumber: "", + projectRoot: "", + account: "", + json: true, + nonInteractive: true, + interactive: false, + debug: false, + rc: null, + exportOnExit: false, + import: "", + + isVSCE: true, +}; + +/** + * User-facing CLI options + */ +// TODO(rrousselGit): options should default to "undefined" until initialized, +// instead of relying on invalid default values. +export const currentOptions = globalSignal({ ...defaultOptions }); + +export function registerOptions(context: ExtensionContext): vscode.Disposable { + currentOptions.value.cwd = getConfigPath(); + const cwdSync = vscode.workspace.onDidChangeWorkspaceFolders(() => { + currentOptions.value = { + ...currentOptions.peek(), + cwd: getConfigPath(), + }; + }); + + const firebaseConfigSync = effect(() => { + const previous = currentOptions.peek(); + + const config = firebaseConfig.value?.tryReadValue; + if (config) { + currentOptions.value = { + ...previous, + config, + configPath: `${previous.cwd}/firebase.json`, + }; + } else { + currentOptions.value = { + ...previous, + config: new Config({}), + configPath: "", + }; + } + }); + + const rcSync = effect(() => { + const previous = currentOptions.peek(); + + const rc = firebaseRC.value?.tryReadValue; + if (rc) { + currentOptions.value = { + ...previous, + rc, + project: rc.projects?.default, + projectId: rc.projects?.default, + }; + } else { + currentOptions.value = { + ...previous, + rc: null, + project: "", + }; + } + }); + + const notifySync = effect(() => { + currentOptions.value; + + context.globalState.setKeysForSync(["currentOptions"]); + context.globalState.update("currentOptions", currentOptions.value); + setInquirerOptions(currentOptions.value); + }); + + return vscode.Disposable.from( + cwdSync, + { dispose: firebaseConfigSync }, + { dispose: rcSync }, + { dispose: notifySync } + ); +} + +/** + * Temporary options to pass to a command, don't write. + * Mostly runs it through the CLI's command.prepare() options formatter. + */ +export async function getCommandOptions( + firebaseJSON: Config, + options: Options = currentOptions.value +): Promise { + // Use any string, it doesn't affect `prepare()`. + const command = new Command("deploy"); + let newOptions = Object.assign(options, { config: options.configPath }); + await command.prepare(newOptions); + return newOptions as Options; +} diff --git a/firebase-vscode/src/result.ts b/firebase-vscode/src/result.ts new file mode 100644 index 00000000000..ec2b74dfeec --- /dev/null +++ b/firebase-vscode/src/result.ts @@ -0,0 +1,85 @@ +/** A wrapper object used to differentiate between error and value state. + * + * It has the added benefit of enabling the differentiation of "no value yet" + * from "value is undefined". + */ +export abstract class Result { + /** Run a block of code and converts the result in a Result. + * + * Errors will be caught, logged and returned as an error. + */ + static guard(cb: () => Promise): Promise>; + static guard(cb: () => T): Result; + static guard(cb: () => T | Promise): Result | Promise> { + try { + const value = cb(); + if (value instanceof Promise) { + return value + .then>((value) => new ResultValue(value)) + .catch((error) => new ResultError(error)); + } + + return new ResultValue(value); + } catch (error: any) { + return new ResultError(error); + } + } + + get tryReadValue(): T | undefined { + return this.switchCase( + (value) => value, + () => undefined + ); + } + + get requireValue(): T { + return this.switchCase( + (value) => value, + (error) => { + throw new Error("Result in error state", { + cause: error, + }); + } + ); + } + + switchCase( + value: (value: T) => NewT, + error: (error: unknown) => NewT + ): NewT { + const that: unknown = this; + if (that instanceof ResultValue) { + return value(that.value); + } + + return error((that as ResultError).error); + } + + follow(cb: (prev: T) => Result): Result { + return this.switchCase( + (value) => cb(value), + (error) => new ResultError(error) + ); + } + + followAsync( + cb: (prev: T) => Promise> + ): Promise> { + return this.switchCase>>( + (value) => cb(value), + async (error) => new ResultError(error) + ); + } +} + +export class ResultValue extends Result { + constructor(readonly value: T) { + super(); + } +} + +export class ResultError extends Result { + constructor(readonly error: unknown) { + super(); + } +} diff --git a/firebase-vscode/src/stubs/empty-class.js b/firebase-vscode/src/stubs/empty-class.js new file mode 100644 index 00000000000..23451b57f03 --- /dev/null +++ b/firebase-vscode/src/stubs/empty-class.js @@ -0,0 +1,3 @@ +class Noop {} + +module.exports = Noop; diff --git a/firebase-vscode/src/stubs/empty-function.js b/firebase-vscode/src/stubs/empty-function.js new file mode 100644 index 00000000000..2fc0e18e095 --- /dev/null +++ b/firebase-vscode/src/stubs/empty-function.js @@ -0,0 +1,3 @@ +const noop = () => {}; + +module.exports = noop; diff --git a/firebase-vscode/src/stubs/inquirer-stub.js b/firebase-vscode/src/stubs/inquirer-stub.js new file mode 100644 index 00000000000..e173ae49eac --- /dev/null +++ b/firebase-vscode/src/stubs/inquirer-stub.js @@ -0,0 +1,33 @@ +const inquirer = module.exports; + +let pluginLogger = { + debug: () => {}, +}; +const optionsKey = Symbol("options"); +inquirer[optionsKey] = {}; + +inquirer.setInquirerOptions = (inquirerOptions) => { + inquirer[optionsKey] = inquirerOptions; +}; + +inquirer.setInquirerLogger = (logger) => { + pluginLogger = logger; +}; + +inquirer.prompt = async (prompts) => { + const answers = {}; + for (const prompt of prompts) { + if (inquirer[optionsKey].hasOwnProperty(prompt.name)) { + answers[prompt.name] = inquirer[optionsKey][prompt.name]; + } else { + pluginLogger.debug( + `Didn't find "${prompt.name}" in options (message:` + + ` "${prompt.message}"), defaulting to value "${prompt.default}"`, + ); + answers[prompt.name] = prompt.default; + } + } + return answers; +}; + +inquirer.registerPrompt = () => {}; diff --git a/firebase-vscode/src/stubs/marked.js b/firebase-vscode/src/stubs/marked.js new file mode 100644 index 00000000000..8a332ace85e --- /dev/null +++ b/firebase-vscode/src/stubs/marked.js @@ -0,0 +1,5 @@ +function marked() {} + +marked.setOptions = () => {}; + +export { marked }; diff --git a/firebase-vscode/src/test/default_wdio.conf.ts b/firebase-vscode/src/test/default_wdio.conf.ts new file mode 100644 index 00000000000..0c98862a00a --- /dev/null +++ b/firebase-vscode/src/test/default_wdio.conf.ts @@ -0,0 +1,59 @@ +import type { Options } from "@wdio/types"; +import * as path from "path"; +import * as child_process from "child_process"; + +export const vscodeConfigs = { + browserName: "vscode", + // Workaround for https://github.com/webdriverio-community/wdio-vscode-service/issues/101#issuecomment-1928159399 + browserVersion: "1.85.0", // also possible: "insiders" or a specific version e.g. "1.80.0" + "wdio:vscodeOptions": { + // points to directory where extension package.json is located + extensionPath: path.join(__dirname, "..", ".."), + // optional VS Code settings + userSettings: { + "editor.fontSize": 14, + }, + }, +}; + +export const config: Options.Testrunner = { + runner: "local", + autoCompileOpts: { + autoCompile: true, + tsNodeOpts: { + project: "./tsconfig.test.json", + transpileOnly: true, + }, + }, + + capabilities: [ + { + browserName: "vscode", + // Workaround for https://github.com/webdriverio-community/wdio-vscode-service/issues/101#issuecomment-1928159399 + browserVersion: "1.85.0", // also possible: "insiders" or a specific version e.g. "1.80.0" + "wdio:vscodeOptions": { + // points to directory where extension package.json is located + extensionPath: path.join(__dirname, "..", ".."), + // optional VS Code settings + userSettings: { + "editor.fontSize": 14, + }, + }, + }, + ], + + // Redirect noisy chromedriver and browser logs to ./logs + outputDir: "./logs", + + afterTest: async function () { + // Reset the test_projects directory to its original state after each test. + // This ensures tests do not modify the test_projects directory. + child_process.execSync( + `git restore --source=HEAD -- ./src/test/test_projects` + ); + }, + + services: ["vscode"], + framework: "mocha", + reporters: ["spec"], +}; diff --git a/firebase-vscode/src/test/empty_wdio.conf.ts b/firebase-vscode/src/test/empty_wdio.conf.ts new file mode 100644 index 00000000000..83b2ac29b0a --- /dev/null +++ b/firebase-vscode/src/test/empty_wdio.conf.ts @@ -0,0 +1,7 @@ +import { config as baseConfig } from "./default_wdio.conf"; +import type { Options } from "@wdio/types"; + +export const config: Options.Testrunner = { + ...baseConfig, + specs: ["./integration/empty/**/*.ts"], +}; diff --git a/firebase-vscode/src/test/fishfood_wdio.conf.ts b/firebase-vscode/src/test/fishfood_wdio.conf.ts new file mode 100644 index 00000000000..6833d52304f --- /dev/null +++ b/firebase-vscode/src/test/fishfood_wdio.conf.ts @@ -0,0 +1,21 @@ +import { merge } from "lodash"; +import { config as baseConfig, vscodeConfigs } from "./default_wdio.conf"; +import type { Options } from "@wdio/types"; +import * as path from "path"; + +const fishfoodPath = path.resolve( + process.cwd(), + "src/test/test_projects/fishfood" +); + +export const config: Options.Testrunner = { + ...baseConfig, + // Disable concurrency as tests may write to the same files. + maxInstances: 1, + specs: ["./integration/fishfood/**/*.ts"], + capabilities: [ + merge(vscodeConfigs, { + "wdio:vscodeOptions": { workspacePath: fishfoodPath }, + }), + ], +}; diff --git a/firebase-vscode/src/test/integration/empty/project.ts b/firebase-vscode/src/test/integration/empty/project.ts new file mode 100644 index 00000000000..a758d707907 --- /dev/null +++ b/firebase-vscode/src/test/integration/empty/project.ts @@ -0,0 +1,22 @@ +import { browser } from "@wdio/globals"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; + +describe("Select project command", () => { + it("waits until projects are loaded", async function () { + const workbench = await browser.getWorkbench(); + const sidebar = new FirebaseSidebar(workbench); + + // This shouldn't be necessary. But at the moment, + // users aren't loaded until the sidebar is opened – + // which blocks the loading of projects. + await sidebar.open(); + + const picker = await workbench.executeCommand("firebase.selectProject"); + + // Wait until at least one option is offered in the picker + // This would timeout if the picker didn't wait for projects to be loaded. + await picker.progress$.waitUntil( + async () => (await picker.getQuickPicks()).length !== 0 + ); + }); +}); diff --git a/firebase-vscode/src/test/integration/empty/sidebar.ts b/firebase-vscode/src/test/integration/empty/sidebar.ts new file mode 100644 index 00000000000..47aa81c3a59 --- /dev/null +++ b/firebase-vscode/src/test/integration/empty/sidebar.ts @@ -0,0 +1,13 @@ +import { browser } from "@wdio/globals"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; + +it("Supports opening empty projects", async function () { + const workbench = await browser.getWorkbench(); + const sidebar = new FirebaseSidebar(workbench); + + await sidebar.open(); + + await sidebar.runInFirebaseViewContext(async (firebase) => { + await firebase.connectProjectLinkElement.waitForDisplayed(); + }); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/deploy.ts b/firebase-vscode/src/test/integration/fishfood/deploy.ts new file mode 100644 index 00000000000..9e394cd2af9 --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/deploy.ts @@ -0,0 +1,41 @@ +import { browser } from "@wdio/globals"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { firebaseTest } from "../../utils/test_hooks"; +import { QuickPick } from "../../utils/page_objects/quick_picks"; +import { e2eSpy, getE2eSpyCalls } from "../mock"; + +firebaseTest("Can deploy services", async function () { + const workbench = await browser.getWorkbench(); + const sidebar = new FirebaseSidebar(workbench); + const quickPicks = new QuickPick(workbench); + + await sidebar.open(); + await sidebar.fdcDeployElement.click(); + + const servicePicks = await quickPicks + .findQuickPicks() + .then((picks) => picks.map((p) => p.getText())); + + expect(servicePicks).toEqual(["us-east"]); + + e2eSpy("deploy"); + + await quickPicks.okElement.click(); + + const connectorPicks = await quickPicks + .findQuickPicks() + .then((picks) => picks.map((p) => p.getText())); + + expect(connectorPicks).toEqual(["a"]); + + await quickPicks.okElement.click(); + + const args = await getE2eSpyCalls("deploy"); + + expect(args.length).toBe(1); + + expect(args[0].length).toBe(3); + expect(args[0][0]).toEqual(["dataconnect"]); + expect(args[0][1].project).toEqual("dart-firebase-admin"); + expect(args[0][2]).toEqual({ context: "us-east" }); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/emulator_status.ts b/firebase-vscode/src/test/integration/fishfood/emulator_status.ts new file mode 100644 index 00000000000..0a0d91d9aae --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/emulator_status.ts @@ -0,0 +1,28 @@ +import { browser } from "@wdio/globals"; +import { StatusBar } from "../../utils/page_objects/status_bar"; +import { firebaseTest } from "../../utils/test_hooks"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; + +firebaseTest( + "If the emulator is not started, the status bar says so", + async function () { + const workbench = await browser.getWorkbench(); + const statusBar = new StatusBar(workbench); + + expect(await statusBar.emulatorsStatus.getText()).toContain( + "Emulators: starting" + ); + } +); + +firebaseTest("When emulators are running, lists them", async function () { + const workbench = await browser.getWorkbench(); + const commands = new FirebaseCommands(); + const statusBar = new StatusBar(workbench); + + await commands.waitEmulators(); + + expect(await statusBar.emulatorsStatus.getText()).toContain( + "Connected to local Postgres" + ); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/execution.ts b/firebase-vscode/src/test/integration/fishfood/execution.ts new file mode 100644 index 00000000000..d3bb9ac5893 --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/execution.ts @@ -0,0 +1,44 @@ +import { browser } from "@wdio/globals"; +import { ExecutionPanel } from "../../utils/page_objects/execution"; +import { firebaseTest } from "../../utils/test_hooks"; +import { EditorView } from "../../utils/page_objects/editor"; +import { queriesPath } from "../../utils/projects"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; + +firebaseTest("Can execute queries", async function () { + const workbench = await browser.getWorkbench(); + const execution = new ExecutionPanel(workbench); + const editor = new EditorView(workbench); + const commands = new FirebaseCommands(); + + await commands.waitEmulators(); + + // Update arguments + await execution.open(); + + await execution.setVariables(`{ + "id": "42" +}`); + + // Execute query + await editor.openFile(queriesPath); + + await editor.firstCodeLense.waitForDisplayed(); + await editor.firstCodeLense.click(); + + // Check the history entry + // TODO - revert history and result view after test + const item = await execution.history.getSelectedItem(); + + // TODO this should work without opening the sidebar + // While the emulator correctly starts without, some leftover state + // still needs the sidebar. + expect(await item.getLabel()).toBe("getPost"); + + // Waiting for the execution to finish + browser.waitUntil(async () => { + (await item.getStatus()) === "success"; + }); + + expect(await item.getDescription()).toContain('Arguments: { "id": "42" }'); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/sidebar.ts b/firebase-vscode/src/test/integration/fishfood/sidebar.ts new file mode 100644 index 00000000000..91b41261b77 --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/sidebar.ts @@ -0,0 +1,20 @@ +import { browser } from "@wdio/globals"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { firebaseTest } from "../../utils/test_hooks"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; + +firebaseTest( + "If emulators are started before opening the sidebar, get a clean initial state", + async function () { + const workbench = await browser.getWorkbench(); + const commands = new FirebaseCommands(); + const sidebar = new FirebaseSidebar(workbench); + + await commands.waitEmulators(); + + await sidebar.open(); + await sidebar.runInFirebaseViewContext(async (firebase) => { + await sidebar.stopEmulatorBtn.waitForDisplayed(); + }); + } +); diff --git a/firebase-vscode/src/test/integration/mock.ts b/firebase-vscode/src/test/integration/mock.ts new file mode 100644 index 00000000000..d1b066fb335 --- /dev/null +++ b/firebase-vscode/src/test/integration/mock.ts @@ -0,0 +1,37 @@ +import { addTearDown } from "../utils/test_hooks"; +import { deploy as cliDeploy } from "../../../../src/deploy"; +import * as vscode from "vscode"; + +export async function e2eSpy(key: string): Promise { + addTearDown(async () => { + await callBrowserSpyCommand(key, { spy: false }); + }); + + await callBrowserSpyCommand(key, { spy: true }); +} + +export function getE2eSpyCalls( + key: "deploy" +): Promise>>; +export async function getE2eSpyCalls(key: string): Promise>> { + return callBrowserSpyCommand( + key, + // We don't mock anything, just read the call list. + { spy: undefined } + ); +} + +async function callBrowserSpyCommand( + key: string, + args: { spy: boolean | undefined } +): Promise>> { + const result = await browser.executeWorkbench( + (vs: typeof vscode, key, args) => { + return vs.commands.executeCommand(key, args); + }, + `fdc-graphql.spy.${key}`, + args + ); + + return result as Array>; +} diff --git a/firebase-vscode/src/test/runTest.ts b/firebase-vscode/src/test/runTest.ts new file mode 100644 index 00000000000..6e533da0d23 --- /dev/null +++ b/firebase-vscode/src/test/runTest.ts @@ -0,0 +1,28 @@ +import * as path from "path"; + +import { runTests } from "@vscode/test-electron"; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, "../../"); + + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, "./suite/src/core/index"); + + // Download VS Code, unzip it and run the integration test + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + // Workaround for https://github.com/webdriverio-community/wdio-vscode-service/issues/101#issuecomment-1928159399 + version: "1.85.0", + }); + } catch (err) { + console.error("Failed to run tests"); + process.exit(1); + } +} + +main(); diff --git a/firebase-vscode/src/test/suite/src/cli.test.ts b/firebase-vscode/src/test/suite/src/cli.test.ts new file mode 100644 index 00000000000..453f4b117f5 --- /dev/null +++ b/firebase-vscode/src/test/suite/src/cli.test.ts @@ -0,0 +1,11 @@ +import * as assert from "assert"; +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; + +firebaseSuite("empty test", () => { + firebaseTest( + "empty test", + async () => { + assert.deepStrictEqual([], []); + } + ); +}); diff --git a/firebase-vscode/src/test/suite/src/core/config.test.ts b/firebase-vscode/src/test/suite/src/core/config.test.ts new file mode 100644 index 00000000000..0b5534e668a --- /dev/null +++ b/firebase-vscode/src/test/suite/src/core/config.test.ts @@ -0,0 +1,658 @@ +import * as assert from "assert"; +import * as vscode from "vscode"; +import * as path from "path"; +import * as fs from "fs"; +import { + _createWatcher, + getConfigPath, + _readFirebaseConfig, + _readRC, + firebaseConfig, + firebaseRC, + getRootFolders, + registerConfig, +} from "../../../../core/config"; +import { + addDisposable, + addTearDown, + firebaseSuite, + firebaseTest, +} from "../../../utils/test_hooks"; +import { createFake, mock } from "../../../utils/mock"; +import { resetGlobals } from "../../../../utils/globals"; +import { workspace } from "../../../../utils/test_hooks"; +import { createFile, createTemporaryDirectory } from "../../../utils/fs"; +import { VsCodeOptions, currentOptions } from "../../../../options"; +import { spyLogs } from "../../../utils/logs"; +import { createTestBroker } from "../../../utils/broker"; +import { setupMockTestWorkspaces } from "../../../utils/workspace"; +import { RC } from "../../../../rc"; +import { Config } from "../../../../config"; +import { ResultValue } from "../../../../result"; + +firebaseSuite("getRootFolders", () => { + firebaseTest("if workspace is empty, returns an empty array", () => { + mock(workspace, undefined); + + const result = getRootFolders(); + + assert.deepEqual(result, []); + }); + + firebaseTest( + "if workspace.workspaceFolders is undefined, returns an empty array", + () => { + mock(workspace, { + workspaceFolders: undefined, + }); + + const result = getRootFolders(); + + assert.deepEqual(result, []); + } + ); + + firebaseTest("returns an array of paths", () => { + mock(workspace, { + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file("/path/to/folder"), + }), + createFake({ + uri: vscode.Uri.file("/path/to/another/folder"), + }), + ], + }); + + const result = getRootFolders(); + + assert.deepEqual(result, ["/path/to/folder", "/path/to/another/folder"]); + }); + + firebaseTest("includes workspaceFile's directory if set", () => { + mock(workspace, { + workspaceFile: vscode.Uri.file("/path/to/folder/file"), + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file("/path/to/another/folder"), + }), + ], + }); + + const result = getRootFolders(); + + assert.deepEqual(result, ["/path/to/another/folder", "/path/to/folder"]); + }); + + firebaseTest("filters path duplicates", () => { + mock(workspace, { + workspaceFile: vscode.Uri.file("/a/file"), + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file("/a"), + }), + createFake({ + uri: vscode.Uri.file("/b"), + }), + createFake({ + uri: vscode.Uri.file("/b"), + }), + ], + }); + + const result = getRootFolders(); + + assert.deepEqual(result, ["/a", "/b"]); + }); +}); + +firebaseSuite("getConfigPath", () => { + // Those tests will impact global variables. We need to reset them after each test. + teardown(() => resetGlobals()); + + firebaseTest( + 'Iterates over getRootFolders, and if a ".firebaserc" ' + + 'or "firebase.json" is found, returns its path', + () => { + const a = createTemporaryDirectory({ debugLabel: "a" }); + const b = createTemporaryDirectory({ debugLabel: "b" }); + createFile(b, ".firebaserc", ""); + const c = createTemporaryDirectory({ debugLabel: "c" }); + createFile(c, "firebase.json", ""); + + const aFolder = createFake({ + uri: vscode.Uri.file(a), + }); + const bFolder = createFake({ + uri: vscode.Uri.file(b), + }); + const cFolder = createFake({ + uri: vscode.Uri.file(c), + }); + + mock(workspace, { workspaceFolders: [aFolder, bFolder, cFolder] }); + assert.deepEqual(getConfigPath(), b, ".firebaserc is found first"); + + mock(workspace, { workspaceFolders: [aFolder, cFolder, bFolder] }); + assert.deepEqual(getConfigPath(), c, "firebase.json is found first"); + } + ); + + firebaseTest("if no firebase config found, returns the first folder", () => { + const a = createTemporaryDirectory({ debugLabel: "a" }); + const b = createTemporaryDirectory({ debugLabel: "b" }); + const c = createTemporaryDirectory({ debugLabel: "c" }); + + const aFolder = createFake({ + uri: vscode.Uri.file(a), + }); + const bFolder = createFake({ + uri: vscode.Uri.file(b), + }); + const cFolder = createFake({ + uri: vscode.Uri.file(c), + }); + + mock(workspace, { workspaceFolders: [aFolder, bFolder, cFolder] }); + assert.deepEqual(getConfigPath(), a); + }); + + firebaseTest('sets "cwd" global variable to the config path', () => { + const a = createTemporaryDirectory(); + const aFolder = createFake({ + uri: vscode.Uri.file(a), + }); + + mock(workspace, { workspaceFolders: [aFolder] }); + + getConfigPath(); + + assert.deepEqual(currentOptions.value.cwd, a); + }); +}); + +firebaseSuite("_readFirebaseConfig", () => { + firebaseTest("parses firebase.json", () => { + const expectedConfig = { + emulators: { + auth: { + port: 9399, + }, + }, + }; + + const dir = createTemporaryDirectory(); + createFile(dir, "firebase.json", JSON.stringify(expectedConfig)); + + mock(workspace, { + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file(dir), + }), + ], + }); + + const config = _readFirebaseConfig(); + assert.deepEqual(config.requireValue.data, expectedConfig); + }); + + firebaseTest("returns undefined if firebase.json is not found", () => { + const dir = createTemporaryDirectory(); + + mock(workspace, { + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file(dir), + }), + ], + }); + + const config = _readFirebaseConfig(); + assert.deepEqual(config, undefined); + }); + + firebaseTest("throws if firebase.json is invalid", () => { + const logs = spyLogs(); + const dir = createTemporaryDirectory(); + createFile(dir, "firebase.json", "invalid json"); + + mock(workspace, { + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file(dir), + }), + ], + }); + + assert.equal(logs.error.length, 0); + + assert.throws( + () => _readFirebaseConfig(), + (thrown) => + thrown + .toString() + .startsWith( + `FirebaseError: There was an error loading ${path.join( + dir, + "firebase.json" + )}:` + ) + ); + + assert.equal(logs.error.length, 1); + assert.ok(logs.error[0].startsWith("There was an error loading")); + }); +}); + +firebaseSuite("_readRC", () => { + firebaseTest("parses .firebaserc", () => { + const expectedConfig = { + projects: { + default: "my-project", + }, + }; + + const dir = createTemporaryDirectory(); + createFile(dir, ".firebaserc", JSON.stringify(expectedConfig)); + + mock(workspace, { + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file(dir), + }), + ], + }); + + const config = _readRC(); + assert.deepEqual( + config?.requireValue.data.projects, + expectedConfig.projects + ); + }); + + firebaseTest("returns undefined if .firebaserc is not found", () => { + const dir = createTemporaryDirectory(); + + mock(workspace, { + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file(dir), + }), + ], + }); + + const config = _readRC(); + assert.deepEqual(config, undefined); + }); + + firebaseTest("throws if .firebaserc is invalid", () => { + const logs = spyLogs(); + const dir = createTemporaryDirectory(); + createFile(dir, ".firebaserc", "invalid json"); + + mock(workspace, { + workspaceFolders: [ + createFake({ + uri: vscode.Uri.file(dir), + }), + ], + }); + + assert.equal(logs.error.length, 0); + + assert.throws( + () => _readRC(), + (thrown) => + thrown.toString() === + `SyntaxError: Unexpected token 'i', "invalid json" is not valid JSON` + ); + + assert.equal(logs.error.length, 1); + assert.equal( + logs.error[0], + `Unexpected token 'i', "invalid json" is not valid JSON` + ); + }); +}); + +firebaseSuite("_createWatcher", () => { + // Those tests will impact global variables. We need to reset them after each test. + teardown(() => resetGlobals()); + + firebaseTest("returns undefined if cwd is not set", () => { + mock(currentOptions, createFake({ cwd: undefined })); + + const watcher = _createWatcher("file"); + + assert.equal(watcher, undefined); + }); + + firebaseTest("creates a watcher for the given file", async () => { + const dir = createTemporaryDirectory(); + const file = createFile(dir, "file", "content"); + + mock(currentOptions, createFake({ cwd: dir })); + + const watcher = _createWatcher("file")!; + addTearDown(() => watcher.dispose()); + + const createdFile = new Promise((resolve) => { + watcher.onDidChange((e) => resolve(e)); + }); + + fs.writeFileSync(file, "new content"); + + assert.equal((await createdFile).path, path.join(dir, "file")); + }); +}); + +firebaseSuite("registerConfig", () => { + // Those tests will impact global variables. We need to reset them after each test. + teardown(() => resetGlobals()); + + firebaseTest( + 'sets "cwd" and firebaseRC/Config global variables on initial call', + async () => { + const expectedConfig = { emulators: { auth: { port: 9399 } } }; + const expectedRc = { projects: { default: "my-project" } }; + const broker = createTestBroker(); + const workspaces = setupMockTestWorkspaces({ + firebaseRc: expectedRc, + firebaseConfig: expectedConfig, + }); + + const disposable = await registerConfig(broker); + addDisposable(disposable); + + // Initial register should not notify anything. + assert.deepEqual(broker.sentLogs, []); + + assert.deepEqual(currentOptions.value.cwd, workspaces.byIndex(0).path); + assert.deepEqual(firebaseConfig.value.requireValue.data, expectedConfig); + assert.deepEqual( + firebaseRC.value.requireValue.data.projects, + expectedRc.projects + ); + } + ); + + firebaseTest( + "when firebaseRC signal changes, calls notifyFirebaseConfig", + async () => { + const initialRC = { projects: { default: "my-project" } }; + const newRC = { projects: { default: "my-new-project" } }; + const broker = createTestBroker(); + setupMockTestWorkspaces({ + firebaseRc: initialRC, + }); + + const disposable = await registerConfig(broker); + addDisposable(disposable); + + assert.deepEqual(broker.sentLogs, []); + + firebaseRC.value = new ResultValue( + new RC(firebaseRC.value.requireValue.path, newRC) + ); + + assert.deepEqual(broker.sentLogs, [ + { + message: "notifyFirebaseConfig", + args: [ + { + firebaseJson: undefined, + firebaseRC: { + etags: {}, + projects: { + default: "my-new-project", + }, + targets: {}, + }, + }, + ], + }, + ]); + } + ); + + firebaseTest( + "when firebaseConfig signal changes, calls notifyFirebaseConfig", + async () => { + const initialConfig = { emulators: { auth: { port: 9399 } } }; + const newConfig = { emulators: { auth: { port: 9499 } } }; + const broker = createTestBroker(); + const workspaces = setupMockTestWorkspaces({ + firebaseConfig: initialConfig, + }); + + const disposable = await registerConfig(broker); + addDisposable(disposable); + + assert.deepEqual(broker.sentLogs, []); + + fs.writeFileSync( + workspaces.byIndex(0).firebaseConfigPath, + JSON.stringify(newConfig) + ); + firebaseConfig.value = _readFirebaseConfig()!; + + assert.deepEqual(broker.sentLogs, [ + { + message: "notifyFirebaseConfig", + args: [ + { + firebaseJson: { + emulators: { + auth: { + port: 9499, + }, + }, + }, + firebaseRC: undefined, + }, + ], + }, + ]); + } + ); + + firebaseTest("supports undefined working directory", async () => { + const broker = createTestBroker(); + mock(currentOptions, { ...currentOptions.value, cwd: undefined }); + + const disposable = await registerConfig(broker); + addDisposable(disposable); + + // Should not throw. + }); + + firebaseTest("disposes of the watchers when disposed", async () => { + const broker = createTestBroker(); + const dir = createTemporaryDirectory(); + + const pendingWatchers = []; + mock( + workspace, + createFake({ + workspaceFolders: [ + createFake({ uri: vscode.Uri.file(dir) }), + ], + // Override "createFileSystemWatcher" to spy on the watchers. + createFileSystemWatcher: () => { + const watcher = createFake({ + onDidCreate: () => ({ dispose: () => {} }), + onDidChange: () => ({ dispose: () => {} }), + dispose: () => { + const index = pendingWatchers.indexOf(watcher); + pendingWatchers.splice(index, 1); + }, + }); + + pendingWatchers.push(watcher); + return watcher; + }, + }) + ); + + const disposable = await registerConfig(broker); + addDisposable(disposable); + + assert.equal(pendingWatchers.length, 3); + assert.deepEqual(Object.keys(broker.onListeners), ["getInitialData"]); + + disposable.dispose(); + + assert.equal(pendingWatchers.length, 0); + assert.deepEqual(Object.keys(broker.onListeners), []); + + firebaseConfig.value = new ResultValue(new Config("")); + firebaseRC.value = new ResultValue(new RC()); + + // Notifying firebaseConfig and firebaseRC should not call notifyFirebaseConfig + assert.deepEqual(broker.sentLogs, []); + }); + + firebaseTest( + "listens to create/update/delete events on firebase.json/.firebaserc/dataconnect.yaml", + async () => { + const watcherListeners: Record< + string, + { + create?: (uri: vscode.Uri) => void; + update?: (uri: vscode.Uri) => void; + delete?: (uri: vscode.Uri) => void; + } + > = {}; + + function addFSListener( + pattern: string, + type: "create" | "update" | "delete", + cb: (uri: vscode.Uri) => void + ) { + const listeners = (watcherListeners[pattern] ??= {}); + assert.equal(watcherListeners[pattern]?.create, undefined); + listeners[type] = cb; + return { dispose: () => {} }; + } + + const dir = createTemporaryDirectory(); + mock( + workspace, + createFake({ + workspaceFolders: [ + createFake({ uri: vscode.Uri.file(dir) }), + ], + // Override "createFileSystemWatcher" to spy on the watchers. + createFileSystemWatcher: (pattern) => { + const file = (pattern as vscode.RelativePattern).pattern; + return createFake({ + onDidCreate: (cb) => addFSListener(file, "create", cb), + onDidChange: (cb) => addFSListener(file, "update", cb), + onDidDelete: (cb) => addFSListener(file, "delete", cb), + dispose: () => {}, + }); + }, + }) + ); + + const broker = createTestBroker(); + + const disposable = await registerConfig(broker); + addDisposable(disposable); + + const rcListeners = watcherListeners[".firebaserc"]!; + const rcFile = path.join(dir, ".firebaserc"); + const configListeners = watcherListeners["firebase.json"]!; + const configFile = path.join(dir, "firebase.json"); + + function testEvent( + index: number, + file: string, + content: string, + fireWatcher: () => void + ) { + assert.equal(broker.sentLogs.length, index); + + fs.writeFileSync(file, content); + fireWatcher(); + + assert.equal(broker.sentLogs.length, index + 1); + } + + function testRcEvent( + event: "create" | "update" | "delete", + index: number + ) { + testEvent( + index, + rcFile, + JSON.stringify({ projects: { default: event } }), + () => rcListeners[event]!(vscode.Uri.file(rcFile)) + ); + + assert.deepEqual(broker.sentLogs[index].args[0].firebaseRC.projects, { + default: event, + }); + } + + function testConfigEvent( + event: "create" | "update" | "delete", + index: number + ) { + testEvent( + index, + configFile, + JSON.stringify({ emulators: { auth: { port: index } } }), + () => configListeners[event]!(vscode.Uri.file(configFile)) + ); + + assert.deepEqual(broker.sentLogs[index].args[0].firebaseJson, { + emulators: { auth: { port: index } }, + }); + } + + testRcEvent("create", 0); + testRcEvent("update", 1); + + testConfigEvent("create", 2); + testConfigEvent("update", 3); + }, + ); + + firebaseTest("handles getInitialData requests", async () => { + const broker = createTestBroker(); + setupMockTestWorkspaces({ + firebaseRc: { projects: { default: "my-project" } }, + firebaseConfig: { emulators: { auth: { port: 9399 } } }, + }); + + const disposable = await registerConfig(broker); + addDisposable(disposable); + + broker.simulateOn("getInitialData"); + + assert.deepEqual(broker.sentLogs, [ + { + message: "notifyFirebaseConfig", + args: [ + { + firebaseJson: { + emulators: { + auth: { + port: 9399, + }, + }, + }, + firebaseRC: { + etags: {}, + projects: { + default: "my-project", + }, + targets: {}, + }, + }, + ], + }, + ]); + }); +}); diff --git a/firebase-vscode/src/test/suite/src/core/index.ts b/firebase-vscode/src/test/suite/src/core/index.ts new file mode 100644 index 00000000000..2cb7d7d8b3b --- /dev/null +++ b/firebase-vscode/src/test/suite/src/core/index.ts @@ -0,0 +1,38 @@ +import * as path from "path"; +import * as Mocha from "mocha"; +import * as glob from "glob"; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: "tdd", + color: true, + }); + + const testsRoot = path.resolve(__dirname, ".."); + + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); +} diff --git a/firebase-vscode/src/test/suite/src/core/project.test.ts b/firebase-vscode/src/test/suite/src/core/project.test.ts new file mode 100644 index 00000000000..0adf2f1ab9f --- /dev/null +++ b/firebase-vscode/src/test/suite/src/core/project.test.ts @@ -0,0 +1,20 @@ +import assert from "assert"; +import { _promptUserForProject } from "../../../../core/project"; +import { firebaseSuite, firebaseTest } from "../../../utils/test_hooks"; +import * as vscode from "vscode"; + +firebaseSuite("_promptUserForProject", () => { + firebaseTest("supports not selecting a project", async () => { + const tokenSource = new vscode.CancellationTokenSource(); + + const result = _promptUserForProject( + new Promise((resolve) => resolve([])), + tokenSource.token + ); + + // Cancel the prompt + tokenSource.cancel(); + + assert.equal(await result, undefined); + }); +}); diff --git a/firebase-vscode/src/test/suite/src/dataconnect/config.test.ts b/firebase-vscode/src/test/suite/src/dataconnect/config.test.ts new file mode 100644 index 00000000000..cea57d67e6a --- /dev/null +++ b/firebase-vscode/src/test/suite/src/dataconnect/config.test.ts @@ -0,0 +1,112 @@ +import assert from "assert"; +import { createTestBroker } from "../../../utils/broker"; +import { firebaseSuite, addDisposable } from "../../../utils/test_hooks"; +import { setupMockTestWorkspaces } from "../../../utils/workspace"; +import { dataConnectConfigs, registerDataConnectConfigs } from "../../../../data-connect/config"; +import { createTemporaryDirectory } from "../../../utils/fs"; +import { createFake, mock } from "../../../utils/mock"; +import { workspace } from "../../../../utils/test_hooks"; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; + +firebaseSuite("registerDataConnectConfigs", async () => { + firebaseSuite("handles getInitialData requests", async () => { + const broker = createTestBroker(); + setupMockTestWorkspaces({ + firebaseRc: { projects: { default: "my-project" } }, + firebaseConfig: { emulators: { dataconnect: { port: 9399 } } }, + }); + + const disposable = registerDataConnectConfigs(broker); + addDisposable(disposable); + + broker.simulateOn("getInitialData"); + + assert.deepEqual(broker.sentLogs, [ + { + message: "notifyFirebaseConfig", + args: [ + { + firebaseJson: { + emulators: { + dataconnect: { + port: 9399, + }, + }, + }, + firebaseRC: { + etags: {}, + projects: { + default: "my-project", + }, + targets: {}, + }, + }, + ], + }, + ]); + }); + + firebaseSuite( + "listens to create/update/delete events on firebase.json/.firebaserc/firemat.yaml", + async () => { + const watcherListeners: Record< + string, + { + create?: (uri: vscode.Uri) => void; + update?: (uri: vscode.Uri) => void; + delete?: (uri: vscode.Uri) => void; + } + > = {}; + + function addFSListener( + pattern: string, + type: "create" | "update" | "delete", + cb: (uri: vscode.Uri) => void, + ) { + const listeners = (watcherListeners[pattern] ??= {}); + assert.equal(watcherListeners[pattern]?.create, undefined); + listeners[type] = cb; + return { dispose: () => {} }; + } + + const dir = createTemporaryDirectory(); + mock( + workspace, + createFake({ + workspaceFolders: [ + createFake({ uri: vscode.Uri.file(dir) }), + ], + // Override "createFileSystemWatcher" to spy on the watchers. + createFileSystemWatcher: (pattern) => { + const file = (pattern as vscode.RelativePattern).pattern; + return createFake({ + onDidCreate: (cb) => addFSListener(file, "create", cb), + onDidChange: (cb) => addFSListener(file, "update", cb), + onDidDelete: (cb) => addFSListener(file, "delete", cb), + dispose: () => {}, + }); + }, + }), + ); + + const broker = createTestBroker(); + const disposable = await registerDataConnectConfigs(broker); + addDisposable(disposable); + + const dataConnectListeners = watcherListeners["**/{dataconnect,connector}.yaml"]!; + const dataConnectFile = path.join(dir, "**/{dataconnect,connector}.yaml"); + + function testDataConnectEvent(event: "create" | "update" | "delete") { + fs.writeFileSync(dataConnectFile, `specVersion: ${event}`); + dataConnectListeners[event]!(vscode.Uri.file(dataConnectFile)); + + assert.deepEqual(dataConnectConfigs.value, [{}]); + } + + testDataConnectEvent("create"); + testDataConnectEvent("update"); + }, + ); +}); diff --git a/firebase-vscode/src/test/test_projects/.gitignore b/firebase-vscode/src/test/test_projects/.gitignore new file mode 100644 index 00000000000..532fdcead69 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/.gitignore @@ -0,0 +1,2 @@ +# Don't commit generated files +**/.dataconnect/ \ No newline at end of file diff --git a/firebase-vscode/src/test/test_projects/fishfood/.firebaserc b/firebase-vscode/src/test/test_projects/fishfood/.firebaserc new file mode 100644 index 00000000000..433140fb0b2 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/.firebaserc @@ -0,0 +1,8 @@ +{ + "projects": { + "default": "dart-firebase-admin" + }, + "targets": {}, + "etags": {}, + "dataconnectEmulatorConfig": {} +} \ No newline at end of file diff --git a/firebase-vscode/src/test/test_projects/fishfood/.gitignore b/firebase-vscode/src/test/test_projects/fishfood/.gitignore new file mode 100644 index 00000000000..dbb58ffbfa3 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/.gitignore @@ -0,0 +1,66 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env diff --git a/firebase-vscode/src/test/test_projects/fishfood/.graphqlrc b/firebase-vscode/src/test/test_projects/fishfood/.graphqlrc new file mode 100644 index 00000000000..1f7cc9f639f --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/.graphqlrc @@ -0,0 +1,9 @@ +schema: + - ./dataconnect/schema/**/*.gql + - ./dataconnect/.dataconnect/**/*.gql +documents: + - ./dataconnect/connectors/**/*.gql +extensions: + endpoints: + default: + url: http://127.0.0.1:8080/__/graphql diff --git a/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/connector.yaml b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/connector.yaml new file mode 100644 index 00000000000..d6e6b5dbb3e --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/connector.yaml @@ -0,0 +1,4 @@ +connectorId: "a" +authMode: "PUBLIC" +generate?: + outputDir: ../../a_connector_js_sdk diff --git a/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/mutations.gql b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/mutations.gql new file mode 100644 index 00000000000..2fb9447ab34 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/mutations.gql @@ -0,0 +1,10 @@ +mutation createPost($id: String, $content: String) @auth(level: PUBLIC) { + post_insert(data: { id: $id, content: $content }) +} +mutation deletePost($id: String!) @auth(level: PUBLIC) { + post_delete(id: $id) +} + +mutation createComment($id: String, $content: String) @auth(level: PUBLIC) { + comment_insert(data: { id: $id, content: $content }) +} diff --git a/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/queries.gql b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/queries.gql new file mode 100644 index 00000000000..86b19536f80 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/queries.gql @@ -0,0 +1,22 @@ +query getPost($id: String!) @auth(level: PUBLIC) { + post(id: $id) { + content + comments: comments_on_post { + id + content + } + } +} + +query listPostsForUser($userId: String!) @auth(level: PUBLIC) { + posts(where: { id: { eq: $userId } }) { + id + content + } +} + +query listPostsOnlyId @auth(level: PUBLIC) { + posts { + id + } +} diff --git a/firebase-vscode/src/test/test_projects/fishfood/dataconnect/dataconnect.yaml b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/dataconnect.yaml new file mode 100644 index 00000000000..960fac366fb --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/dataconnect.yaml @@ -0,0 +1,11 @@ +specVersion: v1alpha +serviceId: us-east +connectorDirs: + - ./connectors/a +schema: + source: ./schema + datasource: + postgresql: + database: "my-database" + cloudSql: + instanceId: "dataconnect-test" diff --git a/firebase-vscode/src/test/test_projects/fishfood/dataconnect/schema/schema.gql b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/schema/schema.gql new file mode 100644 index 00000000000..6f3a5c03a31 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/schema/schema.gql @@ -0,0 +1,10 @@ +type Post @table { + id: String! + content: String! +} + +type Comment @table { + id: String! + content: String! + post: Post! +} diff --git a/firebase-vscode/src/test/test_projects/fishfood/firebase.json b/firebase-vscode/src/test/test_projects/fishfood/firebase.json new file mode 100644 index 00000000000..5060f798c51 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/firebase.json @@ -0,0 +1,18 @@ +{ + "dataconnect": { + "source": "./dataconnect", + "location": "api" + }, + "emulators": { + "auth": { + "port": 9099 + }, + "dataconnect": { + "port": 9399 + }, + "ui": { + "enabled": false + }, + "singleProjectMode": true + } +} diff --git a/firebase-vscode/src/test/tsconfig.test.json b/firebase-vscode/src/test/tsconfig.test.json new file mode 100644 index 00000000000..efd5eb6ec14 --- /dev/null +++ b/firebase-vscode/src/test/tsconfig.test.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2020", + "lib": ["es2020"], + "outDir": "../../dist/test", + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "rootDirs": ["../", "../../../src", "../../common"], + "jsx": "react", + /* Cannot be enabled until we enable it in the root tsconfig.json */ + "strict": false, + "skipLibCheck": true, + "types": ["node", "expect-webdriverio", "wdio-vscode-service", "mocha"] + }, + "exclude": ["**/*.test.ts"], + "include": ["../**/*", "../../common/**/*"] +} diff --git a/firebase-vscode/src/test/utils/broker.ts b/firebase-vscode/src/test/utils/broker.ts new file mode 100644 index 00000000000..ae210477c82 --- /dev/null +++ b/firebase-vscode/src/test/utils/broker.ts @@ -0,0 +1,60 @@ +import { BrokerImpl, Receiver } from "../../messaging/broker"; +import { + MessageParamsMap, + WebviewToExtensionParamsMap, +} from "../../../common/messaging/protocol"; +import { createFake } from "./mock"; + +export type SentLog = { message: string; args: any[] }; + +type OnListener = (...args: unknown[]) => void; + +export interface TestBroker + extends BrokerImpl { + sentLogs: Array; + onListeners: Record>; + + simulateOn(message: string, ...args: unknown[]): void; +} + +/** Creates a fake broker for testing purposes. + * + * It enables observing the messages sent to the broker, and simulating messages + * received. + */ +export function createTestBroker(): TestBroker { + const sentLogs: Array = []; + + const listeners: Record> = {}; + + const fake = createFake({ + onListeners: listeners, + on(message, listener) { + const listenersForMessage = (listeners[message] ??= []); + listenersForMessage.push(listener); + + return () => { + const index = listenersForMessage.indexOf(listener); + if (index !== -1) { + listenersForMessage.splice(index, 1); + } + + if (listenersForMessage.length === 0) { + delete listeners[message]; + } + }; + }, + send(message, ...args) { + sentLogs.push({ message, args }); + }, + sentLogs: sentLogs, + simulateOn(message, ...args) { + const listenersForMessage = listeners[message] ?? []; + for (const listener of listenersForMessage) { + listener(...args); + } + }, + }); + + return fake; +} diff --git a/firebase-vscode/src/test/utils/fs.ts b/firebase-vscode/src/test/utils/fs.ts new file mode 100644 index 00000000000..df2b447f8f9 --- /dev/null +++ b/firebase-vscode/src/test/utils/fs.ts @@ -0,0 +1,56 @@ +import * as path from "path"; +import * as fs from "fs"; +import { addTearDown } from "./test_hooks"; + +// TODO if we can afford adding a dependency, we could use something like "memfs" +// to mock the file system instead of using the real one. + +export type CreateTemporaryDirectoryOptions = { + parent?: string; + debugLabel?: string; +}; + +// Date.now() is not enough to guarantee uniqueness, so we add an incrementing number. +let _increment = 0; + +export function createTemporaryDirectory( + options: CreateTemporaryDirectoryOptions = {} +) { + const debugLabel = `${ + options.debugLabel || "data-connect-test" + }-${Date.now()}-${_increment++}`; + + const relativeDir = options.parent + ? path.join(options.parent, debugLabel) + : debugLabel; + + const absoluteDir = path.normalize(path.join(process.cwd(), relativeDir)); + + fs.mkdirSync(absoluteDir, { recursive: true }); + addTearDown(() => fs.rmSync(absoluteDir, { recursive: true })); + + return absoluteDir; +} + +export function createFile(dir: string, name: string, content: string): string; +export function createFile(file: string, content: string): string; +export function createFile( + ...args: [string, string, string] | [string, string] +) { + let content: string; + let filePath: string; + if (args.length === 2) { + filePath = args[0]; + content = args[1]; + } else { + const [dir, name] = args; + filePath = path.join(dir, name); + content = args[2]; + } + + fs.writeFileSync(filePath, content); + // Using "force" in case the file is deleted before tearDown is ran + addTearDown(() => fs.rmSync(filePath, { force: true })); + + return filePath; +} diff --git a/firebase-vscode/src/test/utils/logs.ts b/firebase-vscode/src/test/utils/logs.ts new file mode 100644 index 00000000000..8deea4e1850 --- /dev/null +++ b/firebase-vscode/src/test/utils/logs.ts @@ -0,0 +1,32 @@ +import { LogLevel, pluginLogger } from "../../logger-wrapper"; +import { addTearDown } from "./test_hooks"; + +export type LogSpy = { + [key in LogLevel]: Array; +}; + +export function spyLogs() { + // Restore the logger after the test ends + const loggerBackup = { ...pluginLogger }; + addTearDown(() => { + Object.assign(pluginLogger, loggerBackup); + }); + + // Spy on the logger + const allLogs: LogSpy = { + debug: [], + info: [], + log: [], + warn: [], + error: [], + }; + for (const key in loggerBackup) { + pluginLogger[key] = function (...args: any[]) { + const logs = allLogs[key]; + + logs.push(args.join(" ")); + }; + } + + return allLogs; +} diff --git a/firebase-vscode/src/test/utils/mock.ts b/firebase-vscode/src/test/utils/mock.ts new file mode 100644 index 00000000000..c64b6f8ead9 --- /dev/null +++ b/firebase-vscode/src/test/utils/mock.ts @@ -0,0 +1,37 @@ +import { Ref } from "../../utils/test_hooks"; +import { addTearDown } from "./test_hooks"; + +/** A function that creates a new object which partially an interface. + * + * Unimplemented properties will throw an error when accessed. + */ +export function createFake(overrides: Partial = {}): T { + const proxy = new Proxy(overrides, { + get(target, prop) { + if (Reflect.has(overrides, prop)) { + return Reflect.get(overrides, prop); + } + + return Reflect.get(target, prop); + }, + + set(target, prop, newVal) { + return Reflect.set(target, prop, newVal); + }, + }); + + return proxy as T; +} + +/** A function designed to mock objects inside unit tests */ +export function mock(ref: Ref, value: Partial | undefined) { + const current = ref.value; + addTearDown(() => { + ref.value = current; + }); + + const fake = !value ? value : createFake(value); + + // Unsafe cast, but it's fine because we're only using this in tests. + ref.value = fake as T; +} diff --git a/firebase-vscode/src/test/utils/page_objects/commands.ts b/firebase-vscode/src/test/utils/page_objects/commands.ts new file mode 100644 index 00000000000..2f61bfd2a22 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/commands.ts @@ -0,0 +1,10 @@ +import * as vscode from "vscode"; +import { addTearDown } from "../test_hooks"; + +export class FirebaseCommands { + async waitEmulators() { + await browser.executeWorkbench(async (vs: typeof vscode) => { + return vs.commands.executeCommand("firebase.emulators.wait"); + }); + } +} diff --git a/firebase-vscode/src/test/utils/page_objects/editor.ts b/firebase-vscode/src/test/utils/page_objects/editor.ts new file mode 100644 index 00000000000..d096474b8e3 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/editor.ts @@ -0,0 +1,26 @@ +import * as vscode from "vscode"; +import { Workbench } from "wdio-vscode-service"; +import { addTearDown } from "../test_hooks"; + +export class EditorView { + constructor(readonly workbench: Workbench) {} + + private readonly editorView = this.workbench.getEditorView(); + + get firstCodeLense() { + return this.editorView.elem.$(".codelens-decoration"); + } + + get codeLensesElements() { + return this.editorView.elem.$$(".codelens-decoration"); + } + + async openFile(path: string) { + // TODO - close opened editors after tests + return browser.executeWorkbench(async (vs: typeof vscode, path) => { + const doc = await vs.workspace.openTextDocument(path); + + return vs.window.showTextDocument(doc, 1, false); + }, path); + } +} diff --git a/firebase-vscode/src/test/utils/page_objects/execution.ts b/firebase-vscode/src/test/utils/page_objects/execution.ts new file mode 100644 index 00000000000..bc75680681c --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/execution.ts @@ -0,0 +1,114 @@ +import { Workbench } from "wdio-vscode-service"; +import { findWebviewWithTitle, runInFrame } from "../webviews"; + +export class ExecutionPanel { + constructor(readonly workbench: Workbench) { + this.history = new HistoryView(workbench); + } + + readonly history: HistoryView; + + async open(): Promise { + await this.workbench.executeCommand( + "data-connect-execution-configuration.focus" + ); + } + + async setVariables(variables: string): Promise { + // TODO revert to the original value after test + + await this.runInConfigurationContext(async (configs) => { + await configs.variablesTextarea.setValue(variables); + }); + } + + async runInConfigurationContext( + cb: (configs: ConfigurationView) => Promise + ): Promise { + const [a, b] = await findWebviewWithTitle("Configuration"); + + return runInFrame(a, () => + runInFrame(b, () => cb(new ConfigurationView(this.workbench))) + ); + } +} + +export class ConfigurationView { + constructor(readonly workbench: Workbench) {} + + get variablesView() { + return $(`vscode-panel-view[aria-labelledby="tab-1"]`); + } + + get variablesTextarea() { + return this.variablesView.$("textarea"); + } +} + +export class HistoryView { + constructor(readonly workbench: Workbench) {} + + get itemsElement() { + return $$(".monaco-list-row"); + } + + get selectedItemElement() { + return $(".monaco-list-row.selected"); + } + + async getSelectedItem(): Promise { + return new HistoryItem(await this.selectedItemElement); + } + + async getItems(): Promise { + // Array.from as workaround to https://github.com/webdriverio-community/wdio-vscode-service/issues/100#issuecomment-1932468126 + const items = Array.from(await this.itemsElement); + + return items.map((item) => new HistoryItem(item)); + } +} + +export class HistoryItem { + constructor(private readonly elem: WebdriverIO.Element) {} + + get iconElement() { + return this.elem.$(".custom-view-tree-node-item-icon"); + } + + get labelElement() { + return this.elem.$(".label-name"); + } + + get descriptionElement() { + return this.elem.$(".label-description"); + } + + async getStatus(): Promise<"success" | "error" | "pending" | "warning"> { + const icon = await this.iconElement; + const clazz = await icon.getAttribute("class"); + + const classes = clazz.split(" "); + + if (classes.includes("codicon-pass")) { + return "success"; + } + + if (classes.includes("codicon-warning")) { + return "warning"; + } + + if (classes.includes("codicon-close")) { + return "error"; + } + + return "pending"; + } + + async getLabel() { + return this.labelElement.getText(); + } + + async getDescription() { + return this.descriptionElement.getText(); + } +} diff --git a/firebase-vscode/src/test/utils/page_objects/quick_picks.ts b/firebase-vscode/src/test/utils/page_objects/quick_picks.ts new file mode 100644 index 00000000000..c27adffc1dd --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/quick_picks.ts @@ -0,0 +1,20 @@ +import { Workbench } from "wdio-vscode-service"; + +/* Workaround to workbench not exposing a way to get an InputBox + * without triggering a command. */ + +export class QuickPick { + constructor(readonly workbench: Workbench) {} + + get okElement() { + return $("a=OK"); + } + + async findQuickPicks() { + // TODO find a way to use InputBox manually that does not trigger a build error + return await $(".quick-input-widget") + .$(".quick-input-list") + .$(".monaco-list-rows") + .$$(".monaco-list-row"); + } +} diff --git a/firebase-vscode/src/test/utils/page_objects/sidebar.ts b/firebase-vscode/src/test/utils/page_objects/sidebar.ts new file mode 100644 index 00000000000..da564c77c9c --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/sidebar.ts @@ -0,0 +1,70 @@ +import { Workbench } from "wdio-vscode-service"; +import { findWebviewWithTitle, runInFrame } from "../webviews"; +import * as vscode from "vscode"; + +export class FirebaseSidebar { + constructor(readonly workbench: Workbench) {} + + async open() { + await browser.executeWorkbench((vs: typeof vscode) => { + return vs.commands.executeCommand( + "firebase.dataConnect.explorerView.focus" + ); + }); + } + + get hostBtn() { + return $("vscode-button=Host your Web App"); + } + + get startEmulatorBtn() { + return $("vscode-button=Launch Data Connect emulator"); + } + + get stopEmulatorBtn() { + return $("vscode-button=Click to stop the emulators"); + } + + get fdcDeployElement() { + return $(".codicon-cloud-upload"); + } + + /** Starts the emulators and waits for the emulators to be started. + * + * This starts emulators by clicking on the button instead of using + * the command. + */ + async startEmulators() { + await this.open(); + + await this.runInFirebaseViewContext(async () => { + await this.startEmulatorBtn.click(); + + // Wait for the emulators to be started + await this.stopEmulatorBtn.waitForDisplayed(); + }); + } + + /** Runs the callback in the context of the Firebase view, within the sidebar */ + async runInFirebaseViewContext( + cb: (firebase: FirebaseView) => Promise + ): Promise { + const [a, b] = await findWebviewWithTitle("Config"); + + return runInFrame(a, () => + runInFrame(b, () => cb(new FirebaseView(this.workbench))) + ); + } +} + +export class FirebaseView { + constructor(readonly workbench: Workbench) {} + + get userIconElement() { + return $(".codicon-account"); + } + + get connectProjectLinkElement() { + return $("vscode-link=Connect a Firebase project"); + } +} diff --git a/firebase-vscode/src/test/utils/page_objects/status_bar.ts b/firebase-vscode/src/test/utils/page_objects/status_bar.ts new file mode 100644 index 00000000000..dbcfe0516c3 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/status_bar.ts @@ -0,0 +1,13 @@ +import { Workbench } from "wdio-vscode-service"; + +export class StatusBar { + constructor(readonly workbench: Workbench) {} + + get emulatorsStatus() { + return $('[id="firebase.firebase-vscode.emulators"]'); + } + + get currentProjectElement() { + return $('[id="firebase.firebase-vscode.projectPicker"]'); + } +} diff --git a/firebase-vscode/src/test/utils/projects.ts b/firebase-vscode/src/test/utils/projects.ts new file mode 100644 index 00000000000..735952af52e --- /dev/null +++ b/firebase-vscode/src/test/utils/projects.ts @@ -0,0 +1,11 @@ +import * as path from "path"; + +export const mutationsPath = path.resolve( + process.cwd(), + "src/test/test_projects/fishfood/dataconnect/connectors/a/mutations.gql" +); + +export const queriesPath = path.resolve( + process.cwd(), + "src/test/test_projects/fishfood/dataconnect/connectors/a/queries.gql" +); diff --git a/firebase-vscode/src/test/utils/sidebar.ts b/firebase-vscode/src/test/utils/sidebar.ts new file mode 100644 index 00000000000..3c03f060662 --- /dev/null +++ b/firebase-vscode/src/test/utils/sidebar.ts @@ -0,0 +1,16 @@ +import { Workbench } from "wdio-vscode-service"; + +export function openFirebaseSidebar() { + return $("a.codicon-mono-firebase").click(); +} + +export async function switchToFirebaseSidebarFrame(workbench: Workbench) { + const sidebarView = await workbench.getWebviewByTitle(""); + await browser.switchToFrame(sidebarView.elem); + + const firebaseView = await $('iframe[title="Firebase"]'); + await firebaseView.waitForDisplayed(); + await browser.switchToFrame(firebaseView); + + return firebaseView; +} diff --git a/firebase-vscode/src/test/utils/test_hooks.ts b/firebase-vscode/src/test/utils/test_hooks.ts new file mode 100644 index 00000000000..a808ae7e082 --- /dev/null +++ b/firebase-vscode/src/test/utils/test_hooks.ts @@ -0,0 +1,99 @@ +import * as vscode from "vscode"; + +let tearDowns: Array<() => void | Promise> = []; + +/** Registers a logic to run after the current test ends. + * + * This is useful to avoid having to use a try/finally block. + * + * The callback is bound to the suite, and when that suite/test ends, the callback is unregistered. + */ +export function addTearDown(cb: () => void | Promise) { + tearDowns.push(cb); +} + +/** Registers a disposable to dispose after the current test ends. + * + * This is sugar for `addTearDown(() => disposable?.dispose())`. + */ +export function addDisposable(disposable: vscode.Disposable | undefined) { + if (disposable) { + addTearDown(() => disposable.dispose()); + } +} + +let setups: Array<() => void | Promise> = []; + +/** Registers initialization logic to run before every tests in that suite. + * + * The callback is bound to the suite, and when that suite ends, the callback is unregistered. + */ +export function setup(cb: () => void | Promise) { + setups.push(cb); +} + +/** A custom "test" to work around "afterEach" not working with the current configs */ +export function firebaseTest( + description: string, + cb: (this: Mocha.Context) => void | Promise +) { + // Since tests may execute in any order, we dereference the list of setup callbacks + // to unsure that other suites' setups don't affect this test. + const testSetups = [...setups]; + const testTearDowns = [...tearDowns]; + + test(description, async function () { + // Tests may call addTearDown to register a callback to run after the test ends. + // We make sure those callbacks are applied only to this test. + const previousTearDowns = tearDowns; + tearDowns = testTearDowns; + + await runGuarded(testSetups); + + try { + await cb.call(this); + } finally { + await runGuarded(testTearDowns.reverse()); + tearDowns = previousTearDowns; + } + }); +} + +export function firebaseSuite(description: string, cb: () => void) { + suite(description, () => { + // Scope setups to the suite. + const previousSetups = setups; + const previousTearDowns = tearDowns; + // Nested suites inherits the setups/teardown from the parent suite. + setups = [...previousSetups]; + tearDowns = [...previousTearDowns]; + + try { + cb(); + } finally { + // The suite has finished registering tests, so we restore the previous setups. + setups = previousSetups; + tearDowns = previousTearDowns; + } + }); +} + +/** Runs callbacks while making sure all of them are executed even if one throws. + * + * If at least one error is thrown, the first one is rethrown. + */ +async function runGuarded(callbacks: Array<() => void | Promise>) { + let firstError: Error | undefined; + + for (const cb of callbacks) { + try { + await cb(); + } catch (e) { + firstError ??= e; + } + } + + if (firstError) { + throw firstError; + } +} diff --git a/firebase-vscode/src/test/utils/webviews.ts b/firebase-vscode/src/test/utils/webviews.ts new file mode 100644 index 00000000000..094828fe553 --- /dev/null +++ b/firebase-vscode/src/test/utils/webviews.ts @@ -0,0 +1,46 @@ +/** An utility to find a Webview with a given name. + * + * This uses a nested loop because the webviews are nested in iframes. + * + * Returns the path of elements pointing to the titled webview. + * This is typically then sent to [runInFrame]. + */ +export async function findWebviewWithTitle(title: string) { + const start = Date.now(); + + /* Keep running until at least 5 seconds have passed. */ + while (Date.now() - start < 5000) { + // Using Array.from because $$ returns a fake array object + const iFrames = Array.from(await $$("iframe.webview.ready")); + + for (const iframe of iFrames) { + try { + await browser.switchToFrame(iframe); + + const frameWithTitle = $(`iframe[title="${title}"]`); + if (await frameWithTitle.isExisting()) { + return [iframe, await frameWithTitle]; + } + } finally { + await browser.switchToParentFrame(); + } + } + } + + throw new Error(`Could not find webview with title: ${title}`); +} + +export async function runInFrame( + element: object, + cb: () => Promise +): Promise { + await browser.switchToFrame(element); + + // Using try/finally to ensure we switch back to the parent frame + // no matter if the test passes or fails. + try { + return await cb(); + } finally { + await browser.switchToParentFrame(); + } +} diff --git a/firebase-vscode/src/test/utils/workspace.ts b/firebase-vscode/src/test/utils/workspace.ts new file mode 100644 index 00000000000..3206778e09f --- /dev/null +++ b/firebase-vscode/src/test/utils/workspace.ts @@ -0,0 +1,78 @@ +import path from "path"; +import { workspace } from "../../utils/test_hooks"; +import { createFile, createTemporaryDirectory } from "./fs"; +import { createFake, mock } from "./mock"; +import * as vscode from "vscode"; + +export type TestWorkspaceConfig = { + debugLabel?: string; + firebaseRc?: unknown; + firebaseConfig?: unknown; + files?: Record; +}; + +export interface TestWorkspace { + debugName?: string; + path: string; + firebaseRCPath: string; + firebaseConfigPath: string; +} + +export interface TestWorkspaces { + byName(name: string): TestWorkspace | undefined; + byIndex(index: number): TestWorkspace | undefined; +} + +/* Sets up a mock workspace with the given files and firebase config. */ +export function setupMockTestWorkspaces( + ...workspaces: TestWorkspaceConfig[] +): TestWorkspaces { + const workspaceFolders = workspaces.map((workspace) => { + const dir = createTemporaryDirectory({ + debugLabel: workspace.debugLabel, + }); + + const firebaseRCPath = path.join(dir, ".firebaserc"); + const firebaseConfigPath = path.join(dir, "firebase.json"); + + if (workspace.firebaseRc) { + createFile(firebaseRCPath, JSON.stringify(workspace.firebaseRc)); + } + if (workspace.firebaseConfig) { + createFile(firebaseConfigPath, JSON.stringify(workspace.firebaseConfig)); + } + + if (workspace.files) { + for (const [filename, content] of Object.entries(workspace.files)) { + createFile(dir, filename, content); + } + } + + return { + path: dir, + firebaseRCPath, + firebaseConfigPath, + }; + }); + + mock(workspace, { + workspaceFolders: workspaceFolders.map((workspace) => + createFake({ + uri: vscode.Uri.file(workspace.path), + }) + ), + createFileSystemWatcher: (...args) => { + // We don't mock watchers, so we defer to the real implementation. + return vscode.workspace.createFileSystemWatcher(...args); + }, + }); + + return { + byName(name: string) { + return workspaceFolders.find((wf) => wf.debugName === name); + }, + byIndex(index: number) { + return workspaceFolders[index]; + }, + }; +} diff --git a/firebase-vscode/src/test/webpack.test.js b/firebase-vscode/src/test/webpack.test.js new file mode 100644 index 00000000000..46d444d7cad --- /dev/null +++ b/firebase-vscode/src/test/webpack.test.js @@ -0,0 +1,58 @@ +const { merge } = require("webpack-merge"); +const path = require("path"); +const configs = require("../../webpack.common"); +const glob = require("glob"); + +const extensionConfig = configs.find((config) => config.name === "extension"); + +const getTestFiles = () => + new Promise((resolve, reject) => { + glob( + "**/**.test.ts", + { cwd: path.resolve(__dirname, "suite") }, + (err, files) => { + if (err) { + reject(e(err)); + } + const testFiles = {}; + for (const file of files) { + const fileName = path.parse(file).name; + testFiles[fileName] = path.resolve(__dirname, "suite", file); + } + resolve(testFiles); + } + ); + }); + +async function getTestConfig() { + const testFiles = await getTestFiles(); + + const testConfig = merge(extensionConfig, { + mode: "development", + name: "test", + entry: testFiles, + output: { + // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve( + __dirname, + "../../dist/test/firebase-vscode/src/test/" + ), + filename: "[name].js", + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + externals: { + vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + fsevents: "require('fsevents')", + }, + optimization: { + splitChunks: { + chunks: "all", + }, + }, + }); + + return testConfig; +} + +module.exports = getTestConfig(); diff --git a/firebase-vscode/src/utils/env.ts b/firebase-vscode/src/utils/env.ts new file mode 100644 index 00000000000..0c6d602e67f --- /dev/null +++ b/firebase-vscode/src/utils/env.ts @@ -0,0 +1,3 @@ +// Set by the `package.json` file +export const isTest = !!process.env.TEST; +export const isDebug = !!process.env.DEBUG; diff --git a/firebase-vscode/src/utils/globals.ts b/firebase-vscode/src/utils/globals.ts new file mode 100644 index 00000000000..1b36fe62409 --- /dev/null +++ b/firebase-vscode/src/utils/globals.ts @@ -0,0 +1,27 @@ +// Various utilities to make globals testable. +// This is a workaround. Ideally, we would not use globals at all. + +import { Signal, signal } from "@preact/signals-react"; + +const globals: Array> = []; + +export function resetGlobals() { + globals.forEach((g) => g.reset()); +} + +export interface GlobalSignal extends Signal { + reset(): void; +} + +export function globalSignal(initialData: T): GlobalSignal { + const s: any = signal(initialData); + + s.reset = () => { + s.value = initialData; + }; + + // TODO: Track globals only in test mode + globals.push(s as GlobalSignal); + + return s as GlobalSignal; +} diff --git a/firebase-vscode/src/utils/graphql.ts b/firebase-vscode/src/utils/graphql.ts new file mode 100644 index 00000000000..603b6c72d13 --- /dev/null +++ b/firebase-vscode/src/utils/graphql.ts @@ -0,0 +1,12 @@ +import * as graphql from "graphql"; +import * as vscode from "vscode"; + +export function locationToRange(location: graphql.Location): vscode.Range { + // -1 because Range uses 0-based indexing but Location uses 1-based indexing + return new vscode.Range( + location.startToken.line - 1, + location.startToken.column - 1, + location.endToken.line - 1, + location.endToken.column - 1 + ); +} diff --git a/firebase-vscode/src/utils/promise.ts b/firebase-vscode/src/utils/promise.ts new file mode 100644 index 00000000000..e0b2f13069e --- /dev/null +++ b/firebase-vscode/src/utils/promise.ts @@ -0,0 +1,18 @@ +export function cancelableThen( + promise: Promise, + then: (t: T) => void, +): { cancel: () => void } { + let canceled = false; + function cancel() { + canceled = true; + } + + promise.then((t) => { + if (!canceled) { + then(t); + } + return t; + }); + + return { cancel }; +} diff --git a/firebase-vscode/src/utils/settings.ts b/firebase-vscode/src/utils/settings.ts new file mode 100644 index 00000000000..767b3036d58 --- /dev/null +++ b/firebase-vscode/src/utils/settings.ts @@ -0,0 +1,32 @@ +import { workspace } from "./test_hooks"; + +interface Settings { + readonly shouldWriteDebug: boolean; + debugLogPath: string; + useFrameworks: boolean; + npmPath: string; +} + +export function getSettings(): Settings { + // Get user-defined VSCode settings if workspace is found. + if (workspace.value.workspaceFolders) { + const workspaceConfig = workspace.value.getConfiguration( + "firebase", + workspace.value.workspaceFolders[0].uri + ); + + return { + shouldWriteDebug: workspaceConfig.get("debug"), + debugLogPath: workspaceConfig.get("debugLogPath"), + useFrameworks: workspaceConfig.get("useFrameworks"), + npmPath: workspaceConfig.get("npmPath"), + }; + } + + return { + shouldWriteDebug: false, + debugLogPath: "", + useFrameworks: false, + npmPath: "", + }; +} diff --git a/firebase-vscode/src/utils/signal.ts b/firebase-vscode/src/utils/signal.ts new file mode 100644 index 00000000000..8a45af35cd1 --- /dev/null +++ b/firebase-vscode/src/utils/signal.ts @@ -0,0 +1,46 @@ +import { Signal } from "@preact/signals-react"; + +/** Waits for a signal value to not be undefined */ +export async function firstWhereDefined( + signal: Signal, +): Promise { + const result = await firstWhere(signal, (v) => v !== undefined); + return result!; +} + +/** Waits for a signal value to respect a certain condition */ +export function firstWhere( + signal: Signal, + predicate: (value: T) => boolean, +): Promise { + return new Promise((resolve) => { + const dispose = signal.subscribe((value) => { + if (predicate(value)) { + resolve(value); + dispose(); + } + }); + }); +} + +/** Calls a callback when the signal value changes. + * + * This will not call the callback immediately, but only after the value changes. + */ +export function onChange( + signal: Signal, + callback: (previous: T, value: T) => void, +): () => void { + var previous: { value: T } | undefined = undefined; + + return signal.subscribe((value) => { + // Updating "previous" before calling the callback, + // to handle cases where the callback throws an error. + const previousValue = previous; + previous = { value }; + + if (previousValue) { + callback(previousValue.value, value); + } + }); +} diff --git a/firebase-vscode/src/utils/test_hooks.ts b/firebase-vscode/src/utils/test_hooks.ts new file mode 100644 index 00000000000..2d55094c2c7 --- /dev/null +++ b/firebase-vscode/src/utils/test_hooks.ts @@ -0,0 +1,47 @@ +import * as vscode from "vscode"; + +/// A value wrapper for mocking purposes. +export type Ref = { value: T }; + +export type Workspace = typeof vscode.workspace; +export const workspace: Ref = { value: vscode.workspace }; + +export interface Mockable any> { + call: (...args: Parameters) => ReturnType; + + dispose(): void; +} + +export function createE2eMockable any>( + cb: T, + key: string, + fallback: () => ReturnType +): Mockable { + let value: (...args: Parameters) => ReturnType = cb; + const calls: Parameters[] = []; + + // A command used by e2e tests to replace the `deploy` function with a mock. + // It is not part of the public API. + const command = vscode.commands.registerCommand( + `fdc-graphql.spy.${key}`, + (options?: { spy?: boolean }) => { + // Explicitly checking true/false to not update the value if `undefined`. + if (options?.spy === false) { + value = cb; + } else if (options?.spy === true) { + value = fallback; + } + + return calls; + } + ); + + return { + call: (...args: Parameters) => { + calls.push(args); + + return value(...args); + }, + dispose: command.dispose, + }; +} diff --git a/firebase-vscode/src/webview.ts b/firebase-vscode/src/webview.ts new file mode 100644 index 00000000000..41d00e161a2 --- /dev/null +++ b/firebase-vscode/src/webview.ts @@ -0,0 +1,105 @@ +import vscode, { Disposable, Uri, Webview, WebviewView } from "vscode"; +import { ExtensionBrokerImpl } from "./extension-broker"; + +function getHtmlForWebview( + entryName: string, + extensionUri: Uri, + webview: Webview +) { + const scriptUri = webview.asWebviewUri( + Uri.joinPath(extensionUri, `dist/web-${entryName}.js`) + ); + const styleUri = webview.asWebviewUri( + Uri.joinPath(extensionUri, `dist/web-${entryName}.css`) + ); + const moniconWoffUri = webview.asWebviewUri( + Uri.joinPath(extensionUri, "resources/Monicons.woff") + ); + const codiconsUri = webview.asWebviewUri( + Uri.joinPath(extensionUri, "resources/dist/codicon.css") + ); + // Use a nonce to only allow a specific script to be run. + const nonce = getNonce(); + + return ` + + + + + + + + + + + + +
+ + +`; +} + +function getNonce() { + let text = ""; + const possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +interface RegisterWebviewParams { + name: string; + broker: ExtensionBrokerImpl; + context: vscode.ExtensionContext; + onResolve?: (view: Webview) => void; +} + +export function registerWebview(params: RegisterWebviewParams): Disposable { + function resolveWebviewView( + webviewView: vscode.WebviewView + ): void | Thenable { + params.broker.registerReceiver(webviewView.webview); + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [params.context.extensionUri], + }; + + webviewView.webview.html = getHtmlForWebview( + params.name, + params.context.extensionUri, + webviewView.webview + ); + + params.onResolve?.(webviewView.webview); + } + + return vscode.window.registerWebviewViewProvider( + params.name, + { + resolveWebviewView, + }, + { webviewOptions: { retainContextWhenHidden: true } } + ); +} diff --git a/firebase-vscode/tsconfig.json b/firebase-vscode/tsconfig.json new file mode 100644 index 00000000000..8ea95f16b7f --- /dev/null +++ b/firebase-vscode/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "typeRoots": ["node_modules/@types", "../src/types"], + "module": "es2020", + "moduleResolution": "node", + "target": "ES2020", + "outDir": "dist", + "lib": ["ES2020"], + "jsx": "react", + "sourceMap": true, + "rootDirs": ["src", "../src", "common"], + "strict": false /* enable all strict type-checking options */ + }, + "ts-node": { + "esm": true + }, + "include": ["src/**/*", "common/**/*"] +} diff --git a/firebase-vscode/webpack.common.js b/firebase-vscode/webpack.common.js new file mode 100644 index 00000000000..82643bba5b1 --- /dev/null +++ b/firebase-vscode/webpack.common.js @@ -0,0 +1,316 @@ +//@ts-check + +"use strict"; + +const path = require("path"); +const webpack = require("webpack"); +const fs = require("fs"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const CopyPlugin = require("copy-webpack-plugin"); +const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); + +/**@type {import('webpack').Configuration}*/ +const extensionConfig = { + name: "extension", + target: "node", // vscode extensions run in webworker context for VS Code web 📖 -> https://webpack.js.org/configuration/target/#target + + entry: { + extension: "./src/extension.ts", + server: { + import: "./src/data-connect/language-server.ts", + filename: "[name].js", + }, + }, // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + output: { + // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, "dist"), + filename: "extension.js", + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + devtool: "source-map", + externalsType: "commonjs", + externals: { + vscode: "vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + // avoid dynamic depencies from @vue/compiler-sfc + squirrelly: "squirrelly", + teacup: "teacup", + "teacup/lib/express": "teacup/lib/express", + "coffee-script": "coffee-script", + marko: "marko", + slm: "slm", + vash: "vash", + plates: "plates", + "babel-core": "babel-core", + htmling: "htmling", + ractive: "ractive", + mote: "mote", + eco: "eco", + jqtpl: "jqtpl", + hamljs: "hamljs", + jazz: "jazz", + hamlet: "hamlet", + whiskers: "whiskers", + "haml-coffee": "haml-coffee", + "hogan.js": "hogan.js", + templayed: "templayed", + walrus: "walrus", + mustache: "mustache", + just: "just", + ect: "ect", + toffee: "toffee", + twing: "twing", + dot: "dot", + "bracket-template": "bracket-template", + velocityjs: "velocityjs", + "dustjs-linkedin": "dustjs-linkedin", + atpl: "atpl", + liquor: "liquor", + twig: "twig", + handlebars: "handlebars", + }, + resolve: { + // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader + // mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules + mainFields: ["main", "module"], + extensions: [".ts", ".js"], + alias: { + // provides alternate implementation for node module and source files + "marked-terminal": path.resolve(__dirname, "src/stubs/empty-class.js"), + // "ora": path.resolve(__dirname, 'src/stubs/empty-function.js'), + commander: path.resolve(__dirname, "src/stubs/empty-class.js"), + inquirer: path.resolve(__dirname, "src/stubs/inquirer-stub.js"), + "inquirer-autocomplete-prompt": path.resolve( + __dirname, + "src/stubs/inquirer-stub.js", + ), + // This is used for Github deploy to hosting - will need to restore + // or find another solution if we add that feature. + "libsodium-wrappers": path.resolve(__dirname, "src/stubs/empty-class.js"), + marked: path.resolve(__dirname, "src/stubs/marked.js"), + }, + fallback: { + // Webpack 5 no longer polyfills Node.js core modules automatically. + // see https://webpack.js.org/configuration/resolve/#resolvefallback + // for the list of Node.js core module polyfills. + }, + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: [/node_modules/], + use: [ + { + loader: "ts-loader", + }, + ], + }, + { + test: /\.ts$/, + loader: "string-replace-loader", + options: { + multiple: [ + // CLI code has absolute path to schema/. We copy schema/ + // into dist, and this is the correct path now. + { + search: /(\.|\.\.)[\.\/]+schema/g, + replace: "./schema", + }, + // Without doing this, it dynamically grabs pkg.name from + // package.json, which is the firebase-vscode package name. + // We want to use the same configstore name as firebase-tools + // so the CLI and extension can share login state. + { + search: /Configstore\(pkg\.name\)/g, + replace: "Configstore('firebase-tools')", + }, + // Some CLI code uses module.exports for test stubbing. + // We are using ES2020 and it doesn't recognize functions called + // as exports.functionName() or module.exports.functionName(). + // Maybe separate those CLI src files at a future time so they can + // still be stubbed for tests without doing this, but this is + // a temporary fix. + { + search: /module\.exports\.([a-zA-Z0-9]+)\(/g, + replace: (match) => match.replace("module.exports.", ""), + }, + // cloudtasks.ts type casts so there's an " as [type]" before the + // starting paren to call the function + { + search: /module\.exports\.([a-zA-Z0-9]+) as/g, + replace: (match) => match.replace("module.exports.", ""), + }, + // Disallow starting . to ensure it doesn't conflict with + // module.exports + // Must end with a paren to avoid overwriting exports assignments + // such as "exports.something = value" + { + search: /[^\.]exports\.([a-zA-Z0-9]+)\(/g, + replace: (match) => match.replace("exports.", ""), + }, + ], + }, + }, + { + test: /\.js$/, + loader: "string-replace-loader", + options: { + multiple: [ + // firebase-tools/node_modules/superstatic/lib/utils/patterns.js + // Stub out the optional RE2 dependency + // TODO: copy the dependency into dist instead of removing them via search/replace. + { + search: 'RE2 = require("re2");', + replace: "RE2 = null;", + }, + // firebase-tools/node_modules/superstatic/lib/middleware/index.js + // Stub out these runtime requirements + // TODO: copy the dependencies into dist instead of removing them via search/replace. + { + search: + 'const mware = require("./" + _.kebabCase(name))(spec, config);', + replace: 'return "";', + }, + ], + }, + }, + { + test: /.node$/, + loader: "node-loader", + }, + ], + }, + plugins: [ + new CopyPlugin({ + patterns: [ + { + from: "../templates", + to: "./templates", + }, + { + from: "../schema", + to: "./schema", + }, + // Copy uncompiled JS files called at runtime by + // firebase-tools/src/parseTriggers.ts + { + from: "*.js", + to: "./", + context: "../src/deploy/functions/runtimes/node", + }, + // Copy cross-env-shell.js used to run predeploy scripts + // to ensure they work in Windows + { + from: "../node_modules/cross-env/dist", + to: "./cross-env/dist", + }, + ], + }), + ], + infrastructureLogging: { + level: "log", // enables logging required for problem matchers + }, +}; + +function makeWebConfig(entryName, entryPath = "") { + return { + name: entryName, + mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + entry: "./" + path.join("webviews", entryPath, `${entryName}.entry.tsx`), + output: { + // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, "dist"), + filename: `web-${entryName}.js`, + }, + resolve: { + extensions: [".ts", ".js", ".jsx", ".tsx"], + }, + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: ["ts-loader"], + }, + // SCSS + /** + * This generates d.ts files for the scss. See the + * "WaitForCssTypescriptPlugin" code below for the workaround required + * to prevent a race condition here. + */ + { + test: /\.scss$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: "@teamsupercell/typings-for-css-modules-loader", + options: { + banner: + "// autogenerated by typings-for-css-modules-loader. \n// Please do not change this file!", + }, + }, + { + loader: "css-loader", + options: { + modules: { + mode: "local", + localIdentName: "[local]-[hash:base64:5]", + exportLocalsConvention: "camelCaseOnly", + }, + url: false, + }, + }, + "postcss-loader", + "sass-loader", + ], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: `web-${entryName}.css`, + }), + new ForkTsCheckerWebpackPlugin(), + new WaitForCssTypescriptPlugin(), + ], + devtool: "nosources-source-map", + }; +} + +// Using the workaround for the typings-for-css-modules-loader race condition +// issue. It doesn't seem like you have to put any actual code into the hook, +// the fact that the hook runs at all seems to be enough delay for the scss.d.ts +// files to be generated. See: +// https://github.com/TeamSupercell/typings-for-css-modules-loader#typescript-does-not-find-the-typings +class WaitForCssTypescriptPlugin { + apply(compiler) { + const hooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler); + + hooks.start.tap("WaitForCssTypescriptPlugin", (change) => { + console.log("Ran WaitForCssTypescriptPlugin"); + return change; + }); + } +} + +/** Each folder in webviews needs to generate their webconfigs independently */ +const baseWebviews = fs + .readdirSync("webviews") + .filter((filename) => filename.match(/\.entry\.tsx/)) + .map((filename) => filename.replace(/\.entry\.tsx/, "")) + .map((name) => makeWebConfig(name)); + +const dataConnectWebviews = fs + .readdirSync("webviews/data-connect") + .filter((filename) => filename.match(/\.entry\.tsx/)) + .map((filename) => filename.replace(/\.entry\.tsx/, "")) + .map((name) => makeWebConfig(name, "data-connect" /** entryPath */)); + +module.exports = [ + // web extensions is disabled for now. + // webExtensionConfig, + extensionConfig, + ...baseWebviews, + ...dataConnectWebviews, +]; diff --git a/firebase-vscode/webpack.dev.js b/firebase-vscode/webpack.dev.js new file mode 100644 index 00000000000..e30006a25eb --- /dev/null +++ b/firebase-vscode/webpack.dev.js @@ -0,0 +1,8 @@ +const { merge } = require("webpack-merge"); +const common = require("./webpack.common.js"); + +module.exports = common.map((config) => + merge(config, { + mode: "development", + }) +); diff --git a/firebase-vscode/webpack.prod.js b/firebase-vscode/webpack.prod.js new file mode 100644 index 00000000000..7f844f999e0 --- /dev/null +++ b/firebase-vscode/webpack.prod.js @@ -0,0 +1,21 @@ +const { merge } = require("webpack-merge"); +const TerserPlugin = require("terser-webpack-plugin"); +const common = require("./webpack.common.js"); + +module.exports = common.map((config) => + merge(config, { + mode: "production", + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + terserOptions: { + keep_classnames: /AbortSignal/, + keep_fnames: /AbortSignal/, + }, + }), + "...", + ], + }, + }) +); diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx new file mode 100644 index 00000000000..7d8bcac44b7 --- /dev/null +++ b/firebase-vscode/webviews/SidebarApp.tsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from "react"; +import { Spacer } from "./components/ui/Spacer"; +import { broker, useBroker } from "./globals/html-broker"; +import { AccountSection } from "./components/AccountSection"; +import { ProjectSection } from "./components/ProjectSection"; + +import { webLogger } from "./globals/web-logger"; +import { ValueOrError } from "./messaging/protocol"; +import { FirebaseConfig } from "../../src/firebaseConfig"; +import { RCData } from "../../src/rc"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; + +export function SidebarApp() { + const env = useBroker("notifyEnv")?.env; + /** + * null - has not finished checking yet + * empty array - finished checking, no users logged in + * non-empty array - contains logged in users + */ + const allUsers = useBroker("notifyUsers")?.users; + const user = useBroker("notifyUserChanged")?.user; + + const configs = useBroker("notifyFirebaseConfig", { + initialRequest: "getInitialData", + }); + const hasFdcConfigs = + useBroker("notifyHasFdcConfigs", { + initialRequest: "getInitialHasFdcConfigs", + }) ?? false; + const accountSection = ( + + ); + // Just render the account section loading view if it doesn't know user state + if (!allUsers || allUsers.length === 0) { + return ( + <> + + Login to use the Firebase plugin + + {accountSection} + + ); + } + if (!configs?.firebaseJson?.value || !hasFdcConfigs) { + const configLabel = !hasFdcConfigs ? "dataconnect.yaml" : "firebase.json"; + + return ( + <> + {accountSection} +

+ No {configLabel} detected in this project +

+
+ { + broker.send("runFirebaseInit"); + }} + > + Run firebase init + + + ); + } + + return ; +} + +function SidebarContent(props: { + configs: { + firebaseJson: ValueOrError; + firebaseRC: ValueOrError; + }; +}) { + const [framework, setFramework] = useState(null); + + const firebaseJson = props.configs?.firebaseJson; + const firebaseRC = props.configs?.firebaseRC; + + const projectId = firebaseRC?.value?.projects?.default; + + const env = useBroker("notifyEnv")?.env; + /** + * null - has not finished checking yet + * empty array - finished checking, no users logged in + * non-empty array - contains logged in users + */ + const allUsers = useBroker("notifyUsers")?.users; + const user = useBroker("notifyUserChanged")?.user; + + useEffect(() => { + webLogger.debug("loading SidebarApp component"); + broker.send("getInitialData"); + + broker.on("notifyFirebaseConfig", ({ firebaseJson, firebaseRC }) => { + webLogger.debug( + "notifyFirebaseConfig", + JSON.stringify(firebaseJson), + JSON.stringify(firebaseRC), + ); + }); + }, []); + + const accountSection = ( + + ); + + return ( + <> + + {accountSection} + {!!user && ( + + )} + + ); +} diff --git a/firebase-vscode/webviews/components/AccountSection.scss b/firebase-vscode/webviews/components/AccountSection.scss new file mode 100644 index 00000000000..55f8281f40c --- /dev/null +++ b/firebase-vscode/webviews/components/AccountSection.scss @@ -0,0 +1,20 @@ +.account-row { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + position: relative; + + &-label { + display: flex; + align-items: center; + } + + &-icon { + margin-right: 8px; + } + + &-project { + display: flex; + flex-direction: column; + } +} diff --git a/firebase-vscode/webviews/components/AccountSection.tsx b/firebase-vscode/webviews/components/AccountSection.tsx new file mode 100644 index 00000000000..82f798d7ba0 --- /dev/null +++ b/firebase-vscode/webviews/components/AccountSection.tsx @@ -0,0 +1,199 @@ +import { + VSCodeLink, + VSCodeDivider, + VSCodeProgressRing, +} from "@vscode/webview-ui-toolkit/react"; +import React, { ReactElement, useState } from "react"; +import { broker } from "../globals/html-broker"; +import { Icon } from "./ui/Icon"; +import { IconButton } from "./ui/IconButton"; +import { PopupMenu, MenuItem } from "./ui/popup-menu/PopupMenu"; +import { Label } from "./ui/Text"; +import styles from "./AccountSection.scss"; +import { ServiceAccountUser } from "../../common/types"; +import { User } from "../../../src/types/auth"; +import { TEXT } from "../globals/ux-text"; + +interface UserWithType extends User { + type?: string; +} + +export function AccountSection({ + user, + allUsers, + isMonospace, +}: { + user: UserWithType | ServiceAccountUser | null; + allUsers: Array | null; + isMonospace: boolean; +}) { + const [userDropdownVisible, toggleUserDropdown] = useState(false); + const usersLoaded = !!allUsers; + // Default: initial users check hasn't completed + let currentUserElement: ReactElement | string = TEXT.LOGIN_PROGRESS; + if (usersLoaded && (!allUsers.length || !user)) { + // Users loaded but no user was found + if (isMonospace) { + currentUserElement = ( + broker.send("executeLogin")}> + {TEXT.GOOGLE_SIGN_IN} + + ); + } else { + // VS Code: prompt user to log in with Google account + currentUserElement = ( + broker.send("addUser")}> + {TEXT.GOOGLE_SIGN_IN} + + ); + } + } else if (usersLoaded && allUsers.length > 0) { + // Users loaded, at least one user was found + if (user.type === "service_account") { + if (isMonospace) { + currentUserElement = ( + broker.send("executeLogin")}> + {TEXT.GOOGLE_SIGN_IN} + + ); + } else { + currentUserElement = TEXT.VSCE_SERVICE_ACCOUNT_LOGGED_IN; + } + } else { + currentUserElement = user.email; + } + } + let userBoxElement = ( + + ); + return ( +
+ {userBoxElement} + {!usersLoaded && ( + + )} + {usersLoaded && allUsers.length > 0 && ( + <> + toggleUserDropdown(!userDropdownVisible)} + /> + {userDropdownVisible ? ( + toggleUserDropdown(false)} + /> + ) : null} + + )} +
+ ); +} + +// TODO(roman): Convert to a better menu +function UserSelectionMenu({ + user, + allUsers, + onClose, + isMonospace, +}: { + user: UserWithType | ServiceAccountUser; + allUsers: Array; + onClose: Function; + isMonospace: boolean; +}) { + const hasNonServiceAccountUser = allUsers.some( + (user) => (user as ServiceAccountUser).type !== "service_account" + ); + const allUsersSorted = [...allUsers].sort((user1, user2) => + (user1 as ServiceAccountUser).type !== "service_account" ? -1 : 1 + ); + /** + * Some temporary fixes here to not show google signin option in + * Monospace. This includes not showing "Default credentials" as + * a selectable dropdown option since there shouldn't be any other + * options. Instead, show the "show service account email" option + * and show it in a primary text size. + * If google signin in Monospace is solved, these can be reverted. + */ + return ( + <> + + {!isMonospace && ( + { + broker.send("addUser"); + onClose(); + }} + > + {hasNonServiceAccountUser + ? TEXT.ADDITIONAL_USER_SIGN_IN + : TEXT.GOOGLE_SIGN_IN} + + )} + {!isMonospace && } + {allUsersSorted.map((user: UserWithType | ServiceAccountUser) => ( + <> + {!isMonospace && ( + { + broker.send("requestChangeUser", { user }); + onClose(); + }} + key={user.email} + > + {user?.type === "service_account" + ? isMonospace + ? TEXT.MONOSPACE_LOGIN_SELECTION_ITEM + : TEXT.VSCE_SERVICE_ACCOUNT_SELECTION_ITEM + : user.email} + + )} + {user?.type === "service_account" && ( + { + broker.send("showMessage", { + msg: `Service account email: ${user.email}`, + options: { + modal: true, + }, + }); + onClose(); + }} + key="service-account-email" + > + + + )} + + ))} + { + // You can't log out of a service account + user.type !== "service_account" && ( + <> + + { + broker.send("logout", { email: user.email }); + onClose(); + }} + > + Sign Out {user.email} + + + ) + } + + + ); +} diff --git a/firebase-vscode/webviews/components/ProjectSection.tsx b/firebase-vscode/webviews/components/ProjectSection.tsx new file mode 100644 index 00000000000..5b4d01e3489 --- /dev/null +++ b/firebase-vscode/webviews/components/ProjectSection.tsx @@ -0,0 +1,92 @@ +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; +import { broker } from "../globals/html-broker"; +import { IconButton } from "./ui/IconButton"; +import { Icon } from "./ui/Icon"; +import { Label } from "./ui/Text"; +import React from "react"; +import styles from "./AccountSection.scss"; +import { ExternalLink } from "./ui/ExternalLink"; +import { TEXT } from "../globals/ux-text"; +import { User } from "../types/auth"; +import { ServiceAccountUser } from "../types"; + +interface UserWithType extends User { + type?: string; +} +export function ProjectSection({ + user, + projectId, + isMonospace, +}: { + user: UserWithType | ServiceAccountUser | null; + projectId: string | null | undefined; + isMonospace: boolean; +}) { + const userEmail = user.email; + + if (isMonospace && user?.type === "service_account") { + return; + } + return ( +
+ + {!!projectId && ( + initProjectSelection(userEmail)} + /> + )} +
+ ); +} + +export function initProjectSelection(userEmail: string | null) { + if (userEmail) { + broker.send("selectProject"); + } else { + broker.send("showMessage", { + msg: "Not logged in", + options: { + modal: true, + detail: `Log in to allow project selection. Click "Sign in with Google" in the sidebar.`, + }, + }); + return; + } +} + +export function ConnectProject({ userEmail }: { userEmail: string | null }) { + return ( + <> + initProjectSelection(userEmail)}> + {TEXT.CONNECT_FIREBASE_PROJECT} + + + ); +} + +export function ProjectInfo({ projectId }: { projectId: string }) { + return ( + <> + {projectId} + + + ); +} diff --git a/firebase-vscode/webviews/components/QuickstartPanel.tsx b/firebase-vscode/webviews/components/QuickstartPanel.tsx new file mode 100644 index 00000000000..7218f5f730f --- /dev/null +++ b/firebase-vscode/webviews/components/QuickstartPanel.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; + +export const QuickstartPanel = ({ onQuickstartButtonClicked }) => { + return ( + onQuickstartButtonClicked()}> + Try a Quickstart! + + ); +}; diff --git a/firebase-vscode/webviews/components/ui/ExternalLink.tsx b/firebase-vscode/webviews/components/ui/ExternalLink.tsx new file mode 100644 index 00000000000..085794a9587 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/ExternalLink.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; +import { broker } from "../../globals/html-broker"; + +export function ExternalLink({ href, text }: { href: string; text: string }) { + return ( + broker.send("openLink", { href })}> + {text} + + ); +} diff --git a/firebase-vscode/webviews/components/ui/Icon.scss b/firebase-vscode/webviews/components/ui/Icon.scss new file mode 100644 index 00000000000..192935d8723 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/Icon.scss @@ -0,0 +1,21 @@ +.monicon { + font-family: "Monicons"; + font-weight: normal; + font-style: normal; + font-size: 16px; + display: inline-block; + width: 1em; + height: 1em; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: "liga"; + speak: none; + text-decoration: inherit; +} diff --git a/firebase-vscode/webviews/components/ui/Icon.tsx b/firebase-vscode/webviews/components/ui/Icon.tsx new file mode 100644 index 00000000000..fbf8e0734e2 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/Icon.tsx @@ -0,0 +1,441 @@ +import cn from "classnames"; +import React, { HTMLAttributes, PropsWithChildren } from "react"; +import styles from "./Icon.scss"; + +export type IconName = CodiconName | MoniconName; + +export type MoniconName = "mono-firebase"; + +export type CodiconName = + | "account" + | "activate-breakpoints" + | "add" + | "archive" + | "arrow-both" + | "arrow-circle-down" + | "arrow-circle-left" + | "arrow-circle-right" + | "arrow-circle-up" + | "arrow-down" + | "arrow-left" + | "arrow-right" + | "arrow-small-down" + | "arrow-small-left" + | "arrow-small-right" + | "arrow-small-up" + | "arrow-swap" + | "arrow-up" + | "azure-devops" + | "azure" + | "beaker-stop" + | "beaker" + | "bell-dot" + | "bell" + | "bold" + | "book" + | "bookmark" + | "bracket-dot" + | "bracket-error" + | "briefcase" + | "broadcast" + | "browser" + | "bug" + | "calendar" + | "call-incoming" + | "call-outgoing" + | "case-sensitive" + | "check-all" + | "check" + | "checklist" + | "chevron-down" + | "chevron-left" + | "chevron-right" + | "chevron-up" + | "chrome-close" + | "chrome-maximize" + | "chrome-minimize" + | "chrome-restore" + | "circle-filled" + | "circle-large-filled" + | "circle-large-outline" + | "circle-outline" + | "circle-slash" + | "circuit-board" + | "clear-all" + | "clippy" + | "close-all" + | "close" + | "cloud-download" + | "cloud-upload" + | "cloud" + | "code" + | "collapse-all" + | "color-mode" + | "combine" + | "comment-discussion" + | "comment" + | "compass-active" + | "compass-dot" + | "compass" + | "copy" + | "credit-card" + | "dash" + | "dashboard" + | "database" + | "debug-all" + | "debug-alt-small" + | "debug-alt" + | "debug-breakpoint-conditional-unverified" + | "debug-breakpoint-conditional" + | "debug-breakpoint-data-unverified" + | "debug-breakpoint-data" + | "debug-breakpoint-function-unverified" + | "debug-breakpoint-function" + | "debug-breakpoint-log-unverified" + | "debug-breakpoint-log" + | "debug-breakpoint-unsupported" + | "debug-console" + | "debug-continue-small" + | "debug-continue" + | "debug-coverage" + | "debug-disconnect" + | "debug-line-by-line" + | "debug-pause" + | "debug-rerun" + | "debug-restart-frame" + | "debug-restart" + | "debug-reverse-continue" + | "debug-stackframe-active" + | "debug-stackframe-dot" + | "debug-stackframe" + | "debug-start" + | "debug-step-back" + | "debug-step-into" + | "debug-step-out" + | "debug-step-over" + | "debug-stop" + | "debug" + | "desktop-download" + | "device-camera-video" + | "device-camera" + | "device-mobile" + | "diff-added" + | "diff-ignored" + | "diff-modified" + | "diff-removed" + | "diff-renamed" + | "diff" + | "discard" + | "edit" + | "editor-layout" + | "ellipsis" + | "empty-window" + | "error-small" + | "error" + | "exclude" + | "expand-all" + | "export" + | "extensions" + | "eye-closed" + | "eye" + | "feedback" + | "file-binary" + | "file-code" + | "file-media" + | "file-pdf" + | "file-submodule" + | "file-symlink-directory" + | "file-symlink-file" + | "file-zip" + | "file" + | "files" + | "filter-filled" + | "filter" + | "flame" + | "fold-down" + | "fold-up" + | "fold" + | "folder-active" + | "folder-library" + | "folder-opened" + | "folder" + | "gear" + | "gift" + | "gist-secret" + | "git-commit" + | "git-compare" + | "git-merge" + | "git-pull-request-closed" + | "git-pull-request-create" + | "git-pull-request-draft" + | "git-pull-request" + | "github-action" + | "github-alt" + | "github-inverted" + | "github" + | "globe" + | "go-to-file" + | "grabber" + | "graph-left" + | "graph-line" + | "graph-scatter" + | "graph" + | "gripper" + | "group-by-ref-type" + | "heart" + | "history" + | "home" + | "horizontal-rule" + | "hubot" + | "inbox" + | "indent" + | "info" + | "inspect" + | "issue-draft" + | "issue-reopened" + | "issues" + | "italic" + | "jersey" + | "json" + | "kebab-vertical" + | "key" + | "law" + | "layers-active" + | "layers-dot" + | "layers" + | "layout-activitybar-left" + | "layout-activitybar-right" + | "layout-centered" + | "layout-menubar" + | "layout-panel-center" + | "layout-panel-justify" + | "layout-panel-left" + | "layout-panel-off" + | "layout-panel-right" + | "layout-panel" + | "layout-sidebar-left-off" + | "layout-sidebar-left" + | "layout-sidebar-right-off" + | "layout-sidebar-right" + | "layout-statusbar" + | "layout" + | "library" + | "lightbulb-autofix" + | "lightbulb" + | "link-external" + | "link" + | "list-filter" + | "list-flat" + | "list-ordered" + | "list-selection" + | "list-tree" + | "list-unordered" + | "live-share" + | "loading" + | "location" + | "lock-small" + | "lock" + | "magnet" + | "mail-read" + | "mail" + | "markdown" + | "megaphone" + | "mention" + | "menu" + | "merge" + | "milestone" + | "mirror" + | "mortar-board" + | "move" + | "multiple-windows" + | "mute" + | "new-file" + | "new-folder" + | "newline" + | "no-newline" + | "note" + | "notebook-template" + | "notebook" + | "octoface" + | "open-preview" + | "organization" + | "output" + | "package" + | "paintcan" + | "pass-filled" + | "pass" + | "person-add" + | "person" + | "pie-chart" + | "pin" + | "pinned-dirty" + | "pinned" + | "play-circle" + | "play" + | "plug" + | "preserve-case" + | "preview" + | "primitive-square" + | "project" + | "pulse" + | "question" + | "quote" + | "radio-tower" + | "reactions" + | "record-keys" + | "record-small" + | "record" + | "redo" + | "references" + | "refresh" + | "regex" + | "remote-explorer" + | "remote" + | "remove" + | "replace-all" + | "replace" + | "reply" + | "repo-clone" + | "repo-force-push" + | "repo-forked" + | "repo-pull" + | "repo-push" + | "repo" + | "report" + | "request-changes" + | "rocket" + | "root-folder-opened" + | "root-folder" + | "rss" + | "ruby" + | "run-above" + | "run-all" + | "run-below" + | "run-errors" + | "save-all" + | "save-as" + | "save" + | "screen-full" + | "screen-normal" + | "search-stop" + | "search" + | "server-environment" + | "server-process" + | "server" + | "settings-gear" + | "settings" + | "shield" + | "sign-in" + | "sign-out" + | "smiley" + | "sort-precedence" + | "source-control" + | "split-horizontal" + | "split-vertical" + | "squirrel" + | "star-empty" + | "star-full" + | "star-half" + | "stop-circle" + | "symbol-array" + | "symbol-boolean" + | "symbol-class" + | "symbol-color" + | "symbol-constant" + | "symbol-enum-member" + | "symbol-enum" + | "symbol-event" + | "symbol-field" + | "symbol-file" + | "symbol-interface" + | "symbol-key" + | "symbol-keyword" + | "symbol-method" + | "symbol-misc" + | "symbol-namespace" + | "symbol-numeric" + | "symbol-operator" + | "symbol-parameter" + | "symbol-property" + | "symbol-ruler" + | "symbol-snippet" + | "symbol-string" + | "symbol-structure" + | "symbol-variable" + | "sync-ignored" + | "sync" + | "table" + | "tag" + | "target" + | "tasklist" + | "telescope" + | "terminal-bash" + | "terminal-cmd" + | "terminal-debian" + | "terminal-linux" + | "terminal-powershell" + | "terminal-tmux" + | "terminal-ubuntu" + | "terminal" + | "text-size" + | "three-bars" + | "thumbsdown" + | "thumbsup" + | "tools" + | "trash" + | "triangle-down" + | "triangle-left" + | "triangle-right" + | "triangle-up" + | "twitter" + | "type-hierarchy-sub" + | "type-hierarchy-super" + | "type-hierarchy" + | "unfold" + | "ungroup-by-ref-type" + | "unlock" + | "unmute" + | "unverified" + | "variable-group" + | "verified-filled" + | "verified" + | "versions" + | "vm-active" + | "vm-connect" + | "vm-outline" + | "vm-running" + | "vm" + | "wand" + | "warning" + | "watch" + | "whitespace" + | "whole-word" + | "window" + | "word-wrap" + | "workspace-trusted" + | "workspace-unknown" + | "workspace-untrusted" + | "zoom-in" + | "zoom-out"; + +type IconProps = PropsWithChildren< + T & + HTMLAttributes & { + icon: IconName; + } +>; + +export const Icon: React.FC> = ({ + icon, + className, + ...props +}) => { + let mono = icon.startsWith("mono-"); + return mono ? ( + + {icon} + + ) : ( +
+ ); +}; diff --git a/firebase-vscode/webviews/components/ui/IconButton.tsx b/firebase-vscode/webviews/components/ui/IconButton.tsx new file mode 100644 index 00000000000..50c2c9ebff7 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/IconButton.tsx @@ -0,0 +1,30 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import React, { HTMLAttributes, PropsWithChildren } from "react"; +import { Icon, IconName } from "./Icon"; + +type TextProps = PropsWithChildren< + T & + HTMLAttributes & { + icon: IconName; + tooltip: string; + } +>; + +export const IconButton: React.FC> = ({ + icon, + tooltip, + className, + ...props +}) => { + return ( + + + + ); +}; diff --git a/firebase-vscode/webviews/components/ui/PanelSection.scss b/firebase-vscode/webviews/components/ui/PanelSection.scss new file mode 100644 index 00000000000..046c5983327 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/PanelSection.scss @@ -0,0 +1,20 @@ +.panelExpando { + appearance: none; + background-color: transparent; + display: flex; + align-items: center; + line-height: 16px; + padding: 0; + border: 0; + cursor: pointer; + gap: 4px; + color: var(--vscode-descriptionForeground); + + .panelExpandoIcon { + transition: transform 0.1s ease; + } + + &:not(.isExpanded) .panelExpandoIcon { + transform: rotate(-90deg); + } +} diff --git a/firebase-vscode/webviews/components/ui/PanelSection.tsx b/firebase-vscode/webviews/components/ui/PanelSection.tsx new file mode 100644 index 00000000000..b9c87018fc7 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/PanelSection.tsx @@ -0,0 +1,45 @@ +import { VSCodeDivider } from "@vscode/webview-ui-toolkit/react"; +import React, { ReactNode, useState } from "react"; +import { Icon } from "./Icon"; +import { Spacer } from "./Spacer"; +import { Heading } from "./Text"; +import cn from "classnames"; +import styles from "./PanelSection.scss"; + +export function PanelSection({ + title, + children, + isLast, + style, +}: React.PropsWithChildren<{ + title?: ReactNode; + isLast?: boolean; + + style?: React.CSSProperties; +}>) { + let [isExpanded, setExpanded] = useState(true); + + return ( + <> + {title && ( + + )} + {isExpanded && ( + <> + {title && } + {children} + + {!isLast && } + + )} + + ); +} diff --git a/firebase-vscode/webviews/components/ui/Spacer.scss b/firebase-vscode/webviews/components/ui/Spacer.scss new file mode 100644 index 00000000000..cee24494810 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/Spacer.scss @@ -0,0 +1,23 @@ +.spacerxsmall { + height: var(--space-xsmall); +} + +.spacersmall { + height: var(--space-small); +} + +.spacermedium { + height: var(--space-medium); +} + +.spacerlarge { + height: var(--space-large); +} + +.spacerxlarge { + height: var(--space-xlarge); +} + +.spacerxxlarge { + height: var(--space-xxlarge); +} diff --git a/firebase-vscode/webviews/components/ui/Spacer.tsx b/firebase-vscode/webviews/components/ui/Spacer.tsx new file mode 100644 index 00000000000..4b05f1c1ae1 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/Spacer.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import styles from "./Spacer.scss"; + +type SpacerSize = + | "xsmall" + | "small" + | "medium" + | "large" + | "xlarge" + | "xxlarge"; + +export function Spacer({ size = "large" }: { size: SpacerSize }) { + return
; +} diff --git a/firebase-vscode/webviews/components/ui/SplitButton.scss b/firebase-vscode/webviews/components/ui/SplitButton.scss new file mode 100644 index 00000000000..056ec6dc113 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/SplitButton.scss @@ -0,0 +1,38 @@ +.split-button { + display: flex; + position: relative; +} + +.main-target, +.menu-target { + &:focus { + z-index: 1; + } +} + +.main-target { + flex: 1 1 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.menu-target { + position: relative; + padding: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + --divider-vert-margin: 4px; + --button-padding-horizontal: 4px; + + &::before { + position: absolute; + left: 0; + top: var(--divider-vert-margin); + width: 1px; + height: calc(100% - var(--divider-vert-margin) * 2); + content: ""; + background-color: var(--vscode-button-foreground); + opacity: 0.2; + pointer-events: none; + } +} diff --git a/firebase-vscode/webviews/components/ui/SplitButton.tsx b/firebase-vscode/webviews/components/ui/SplitButton.tsx new file mode 100644 index 00000000000..a60144cd57e --- /dev/null +++ b/firebase-vscode/webviews/components/ui/SplitButton.tsx @@ -0,0 +1,53 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import cn from "classnames"; +import React, { HTMLAttributes, PropsWithChildren, useState } from "react"; +import { Icon } from "./Icon"; +import styles from "./SplitButton.scss"; +import { PopupMenu } from "./popup-menu/PopupMenu"; + +type SplitButtonProps = PropsWithChildren< + HTMLAttributes & { + appearance?: "primary" | "secondary"; + onClick: Function; + popupMenuContent: React.ReactNode; + } +>; + +export const SplitButton: React.FC = ({ + children, + onClick, + className, + popupMenuContent, + appearance, + ...props +}) => { + const [menuOpen, setMenuOpen] = useState(false); + + return ( + <> +
+ {menuOpen && ( + setMenuOpen(false)}> + {popupMenuContent} + + )} + + {children} + + setMenuOpen(true)} + appearance={appearance || "secondary"} + {...(props as any)} + > + + +
+ + ); +}; diff --git a/firebase-vscode/webviews/components/ui/Text.scss b/firebase-vscode/webviews/components/ui/Text.scss new file mode 100644 index 00000000000..ffc736c76cb --- /dev/null +++ b/firebase-vscode/webviews/components/ui/Text.scss @@ -0,0 +1,94 @@ +h1, +h2, +h3, +h4, +h5, +h6, +.text { + margin: 0; + padding: 0; + font-weight: normal; + user-select: none; +} + +p, +ol, +ul { + margin: 0; + padding: 0; + line-height: var(--vscode-line-height); +} + +h1 { + font-size: 26px; + line-height: var(--vscode-line-height); + font-weight: 600; // semibold + margin: 0; +} + +h2 { + font-size: 16px; + line-height: var(--vscode-line-height); + font-weight: 500; // medium +} + +h3 { + font-size: 13px; + line-height: var(--vscode-line-height); + font-weight: 800; // heavy +} + +h4 { + font-size: 11px; + line-height: var(--vscode-line-height); + font-weight: 700; // bold + text-transform: uppercase; +} + +h5 { + font-size: 11px; + line-height: var(--vscode-line-height); + font-weight: 500; // medium + text-transform: uppercase; +} + +h6 { + font-size: 11px; + line-height: var(--vscode-line-height); + font-weight: 800; // heavy +} + +.b1 { + font-size: inherit; + line-height: var(--vscode-line-height); +} + +.b2 { + font-size: 12px; + line-height: var(--vscode-line-height); +} + +.l1 { + font-size: 14px; + line-height: var(--vscode-line-height); + font-weight: 500; // medium +} + +.l2 { + font-size: 12px; + font-weight: 700; // bold +} + +.l3 { + font-size: 11px; + font-weight: 500; // medium +} + +.l4 { + font-size: 9px; + font-weight: 700; // bold +} + +.color-secondary { + color: var(--vscode-descriptionForeground); +} diff --git a/firebase-vscode/webviews/components/ui/Text.tsx b/firebase-vscode/webviews/components/ui/Text.tsx new file mode 100644 index 00000000000..15826cd03fd --- /dev/null +++ b/firebase-vscode/webviews/components/ui/Text.tsx @@ -0,0 +1,58 @@ +import cn from "classnames"; +import React, { HTMLAttributes, PropsWithChildren } from "react"; +import styles from "./Text.scss"; + +type TextProps = PropsWithChildren< + T & + HTMLAttributes & { + secondary?: boolean; + as?: any; + } +>; + +const Text: React.FC> = ({ + secondary, + as: Component = "div", + className, + ...props +}) => { + return ( + + ); +}; + +export const Heading: React.FC> = ({ + level = 1, + ...props +}) => { + return ; +}; + +export const Label: React.FC> = ({ + level = 1, + className, + ...props +}) => { + return ( + + ); +}; + +export const Body: React.FC> = ({ + level = 1, + className, + ...props +}) => { + return ( + + ); +}; diff --git a/firebase-vscode/webviews/components/ui/popup-menu/PopupMenu.scss b/firebase-vscode/webviews/components/ui/popup-menu/PopupMenu.scss new file mode 100644 index 00000000000..d6d13894c3a --- /dev/null +++ b/firebase-vscode/webviews/components/ui/popup-menu/PopupMenu.scss @@ -0,0 +1,57 @@ +.menu { + position: absolute; + right: 0; + background-color: var(--vscode-sideBar-background); + padding: 4px 0; + margin: 0; + border-radius: 3px; + box-shadow: 0 0 0 1px var(--divider-background, black), + 0 4px 12px var(--vscode-widget-shadow); + color: var(--vscode-foreground); + z-index: 2; + list-style: none; + display: flex; + flex-direction: column; + align-items: stretch; +} + +.scrim { + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + cursor: default; + z-index: 1; +} + +.item { + width: 100%; + text-align: left; + background-color: transparent; + appearance: none; + border: 0; + cursor: pointer; + font-family: inherit; + color: var(--color-ink); + padding: 4px 16px; + display: flex; + align-items: center; + + :global(.material-icons) { + margin-right: 12px; + color: var(--color-ink-2); + } + + &[disabled] { + cursor: not-allowed; + color: var(--color-ink-disabled); + } + + &:not([disabled]):hover { + background-color: var(--vscode-list-hoverBackground); + } + &:not([disabled]):active { + background-color: var(--vscode-list-activeSelectionBackground); + } +} diff --git a/firebase-vscode/webviews/components/ui/popup-menu/PopupMenu.tsx b/firebase-vscode/webviews/components/ui/popup-menu/PopupMenu.tsx new file mode 100644 index 00000000000..a77ceee7213 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/popup-menu/PopupMenu.tsx @@ -0,0 +1,62 @@ +import cn from "classnames"; +import React, { FC, HTMLAttributes, PropsWithChildren } from "react"; +import styles from "./PopupMenu.scss"; + +// TODO(hsubox76): replace this with a real, accessible Menu component + +type PopupMenuProps = PropsWithChildren< + T & + HTMLAttributes & { + show?: boolean; + onClose: Function; + autoClose: boolean; + } +>; + +export const PopupMenu: FC> = ({ + children, + autoClose, + className, + show, + onClose, +}) => { + return ( + <> + {show && ( + <> +
onClose()} /> +
    { + autoClose && onClose(); + }} + > + {children} +
+ + )} + + ); +}; + +type MenuItemProps = PropsWithChildren< + T & + HTMLAttributes & { + onClick: Function; + } +>; + +export const MenuItem: FC> = ({ + className, + onClick, + children, +}) => { + return ( +
  • + +
  • + ); +}; diff --git a/firebase-vscode/webviews/data-connect/DataConnectExecutionResultsApp.tsx b/firebase-vscode/webviews/data-connect/DataConnectExecutionResultsApp.tsx new file mode 100644 index 00000000000..d44a8565926 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/DataConnectExecutionResultsApp.tsx @@ -0,0 +1,144 @@ +import React from "react"; +import { useBroker } from "../globals/html-broker"; +import { Label } from "../components/ui/Text"; +import style from "./data-connect-execution-results.entry.scss"; +import { SerializedError } from "../../common/error"; +import { ExecutionResult, GraphQLError } from "graphql"; +import { isExecutionResult } from "../../common/graphql"; + +// Prevent webpack from removing the `style` import above +style; + +export function DataConnectExecutionResultsApp() { + const dataConnectResults = useBroker("notifyDataConnectResults", { + // Forcibly read the current execution results when the component mounts. + // This handles cases where the user navigates to the results view after + // an execution result has already been set. + initialRequest: "getDataConnectResults", + }); + const results: ExecutionResult | SerializedError | undefined = + dataConnectResults?.results; + + if (!dataConnectResults || !results) { + return null; + } + + let response: unknown; + let errorsDisplay: JSX.Element | undefined; + + if (isExecutionResult(results)) { + // We display the response even if there are errors, just + // in case the user wants to see the response anyway. + response = results.data; + const errors = results.errors; + + if (errors && errors.length !== 0) { + errorsDisplay = ( + <> + + {errors.map((error) => ( + + ))} + + ); + } + } else { + // We don't display a "response" here, because this is an error + // that occurred without returning a valid GraphQL response. + errorsDisplay = ; + } + + let resultsDisplay: JSX.Element | undefined; + if (response) { + resultsDisplay = ( + <> + + +
    {JSON.stringify(response, null, 2)}
    +
    + + ); + } + + return ( + <> + {errorsDisplay} + {resultsDisplay} + + + +
    {dataConnectResults.query}
    +
    + + + +
    {dataConnectResults.args}
    +
    + + ); +} + +/** A view for when executions either fail before the HTTP request is sent, + * or when the HTTP response is an error. + */ +function InternalErrorView({ error }: { error: SerializedError }) { + return ( + <> + +

    + { + // Stacktraces usually already include the message, so we only + // display the message if there is no stacktrace. + error.stack ? : error.message + } + {error.cause && ( + <> +
    +

    Cause:

    + + + )} +

    + + ); +} + +/** A view for when an execution returns status 200 but contains errors. */ +function GraphQLErrorView({ error }: { error: GraphQLError }) { + let pathDisplay: JSX.Element | undefined; + if (error.path) { + // Renders the path as a series of kbd elements separated by commas + pathDisplay = ( + <> + {error.path?.map((path, index) => { + const item = {path}; + + return index === 0 ? item : <>, {item}; + })}{" "} + + ); + } + + return ( +

    + {pathDisplay} + {error.message} + {error.stack && } +

    + ); +} + +function StackView({ stack }: { stack: string }) { + return ( + + {stack} + + ); +} diff --git a/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.scss b/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.scss new file mode 100644 index 00000000000..a74916c4b51 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.scss @@ -0,0 +1,59 @@ +@import "../globals/index.scss"; + +textarea { + border-radius: 5px; + background: var(--vscode-editor-background); + + // Prevent resizes as it is fullscreen + resize: none; + + font-family: monospace; + font-size: var(--type-ramp-base-font-size); + line-height: var(--type-ramp-base-line-height); + + padding: calc(var(--container-padding) / 2) var(--container-padding); + + color: var(--vscode-input-foreground); + background: var(--vscode-editor-background); + + border: none; + outline: none; +} + +textarea:focus { + border: none; + outline: none; +} + +html, +body, +body > *, +body > * > * { + height: 100%; +} + +vscode-panel-view { + padding-left: 0; + padding-right: 0; +} + +.variable { + display: grid; + align-items: stretch; + justify-content: stretch; +} + +.authentication { + display: flex; + flex-direction: column; + + span + * { + margin-top: 3px; + } +} + +// Make sure hidden panels are correctly hidden +// even if we override the display type for type purposes. +vscode-panel-view[hidden] { + display: none; +} diff --git a/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.tsx b/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.tsx new file mode 100644 index 00000000000..d0dd58f8415 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useRef, useState } from "react"; +import { createRoot } from "react-dom/client"; +import style from "./data-connect-execution-configuration.entry.scss"; +import { + VSCodeDropdown, + VSCodeOption, + VSCodePanels, + VSCodePanelTab, + VSCodePanelView, + VSCodeTextArea, +} from "@vscode/webview-ui-toolkit/react"; +import { broker } from "../globals/html-broker"; +import { Spacer } from "../components/ui/Spacer"; +import { UserMockKind } from "../../common/messaging/protocol"; + +const root = createRoot(document.getElementById("root")!); +root.render(); + +export function DataConnectExecutionArgumentsApp() { + function handleVariableChange(e: React.ChangeEvent) { + broker.send("definedDataConnectArgs", e.target.value); + } + + // Due to webview-ui-toolkit adding shadow-roots, css alone is not + // enough to customize the look of the panels. + // We use some imperative code to manually inject some style. + // This is not ideal, but it's the best we can do for now. + // Those changes are needed for the textarea to fill the available + // space, to have a good scroll behavior. + const ref = useRef(undefined); + useEffect(() => { + if (!ref.current) { + return; + } + + const style = document.createElement("style"); + style.append(` + .tabpanel { + display: grid; + align-items: stretch; + justify-content: stretch; + } + `); + + ref.current.shadowRoot!.append(style); + }, []); + + return ( + + VARIABLES + AUTHENTICATION + + + + + + + + ); +} + +function AuthUserMockForm() { + const [selectedKind, setSelectedMockKind] = useState( + UserMockKind.ADMIN + ); + const [claims, setClaims] = useState( + `{\n "email_verified": true,\n "sub": "exampleUserId"\n}` + ); + + useEffect(() => { + broker.send("notifyAuthUserMockChange", { + kind: selectedKind, + claims: selectedKind === UserMockKind.AUTHENTICATED ? claims : undefined, + }); + }, [selectedKind, claims]); + + let expandedForm: JSX.Element | undefined; + if (selectedKind === UserMockKind.AUTHENTICATED) { + expandedForm = ( + <> + + Claim and values + setClaims(event.target.value)} + /> + + ); + } + + return ( + <> + Run as + setSelectedMockKind(event.target.value)} + > + Admin + + Unauthenticated + + + Authenticated + + + {expandedForm} + + ); +} diff --git a/firebase-vscode/webviews/data-connect/data-connect-execution-results.entry.scss b/firebase-vscode/webviews/data-connect/data-connect-execution-results.entry.scss new file mode 100644 index 00000000000..b5143ecdd67 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect-execution-results.entry.scss @@ -0,0 +1,37 @@ +@import "../globals/index.scss"; + +.l1 { + text-transform: capitalize; +} + +body { + padding: 0; + + // Somehow #root does not seem to be usable within this file. + & > div { + min-width: calc(100% - (var(--container-padding) * 2)); + // Fill the horizontal space, and on text overflow, + // have the text-overflow expand the horizontal space + display: inline-flex !important; + // If one item overflows horizontally, have all items + // use the same width for consistency + align-items: stretch; + + padding: var(--container-padding); + } + + code { + pre { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + padding: 1em; + margin-top: 0; + font-family: monospace; + } + } + + // Placing in "body" to override the global styles + p { + margin-bottom: 1em; + } +} diff --git a/firebase-vscode/webviews/data-connect/data-connect-execution-results.entry.tsx b/firebase-vscode/webviews/data-connect/data-connect-execution-results.entry.tsx new file mode 100644 index 00000000000..5e9bff2df46 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect-execution-results.entry.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { DataConnectExecutionResultsApp } from "./DataConnectExecutionResultsApp"; + +const root = createRoot(document.getElementById("root")!); +root.render(); diff --git a/firebase-vscode/webviews/data-connect/data-connect.entry.tsx b/firebase-vscode/webviews/data-connect/data-connect.entry.tsx new file mode 100644 index 00000000000..5e8ca4e4c79 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect.entry.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"; +import { Spacer } from "../components/ui/Spacer"; +import styles from "../globals/index.scss"; +import { broker, useBroker, useBrokerListener } from "../globals/html-broker"; +import { PanelSection } from "../components/ui/PanelSection"; + +// Prevent webpack from removing the `style` import above +styles; + +const root = createRoot(document.getElementById("root")!); +root.render(); + +function DataConnect() { + const isConnectedToPostgres = + useBroker("notifyIsConnectedToPostgres", { + initialRequest: "getInitialIsConnectedToPostgres", + }) ?? false; + + const psqlString = useBroker("notifyPostgresStringChanged"); + + return ( + <> + + {!isConnectedToPostgres && (

    + Connect to Local PostgreSQL. See also:{" "} + + Working with the emulator + +

    ) + } + + {isConnectedToPostgres ? ( + <> + + + + ) : ( + broker.send("connectToPostgres")}> + Connect to Local PostgreSQL + + )} +
    + +

    + Deploy FDC services and connectors to production. See also:{" "} + + Deploying + +

    + + broker.send("fdc.deploy")}> + Deploy + + + broker.send("fdc.deploy-all")} + > + Deploy all + +
    + + ); +} diff --git a/firebase-vscode/webviews/globals/app.tsx b/firebase-vscode/webviews/globals/app.tsx new file mode 100644 index 00000000000..a9dc46b953a --- /dev/null +++ b/firebase-vscode/webviews/globals/app.tsx @@ -0,0 +1,11 @@ +import React, { ReactNode, StrictMode } from "react"; +import { ExtensionStateProvider } from "./extension-state"; + +/** Generic wrapper that all webviews should be wrapped with */ +export function App({ children }: { children: ReactNode }): JSX.Element { + return ( + + {children} + + ); +} diff --git a/firebase-vscode/webviews/globals/extension-state.tsx b/firebase-vscode/webviews/globals/extension-state.tsx new file mode 100644 index 00000000000..5a05ec43467 --- /dev/null +++ b/firebase-vscode/webviews/globals/extension-state.tsx @@ -0,0 +1,71 @@ +import React, { + createContext, + ReactNode, + useContext, + useEffect, + useMemo, +} from "react"; +import { broker, useBrokerListener } from "./html-broker"; +import { signal, computed } from "@preact/signals-react"; +import { User } from "../types/auth"; + +export enum Environment { + UNSPECIFIED, + VSC, + IDX, +} + +function createExtensionState() { + const environment = signal(Environment.UNSPECIFIED); + const users = signal([]); + const selectedUserEmail = signal(""); + const projectId = signal(""); + + const selectedUser = computed(() => + users.value.find((user) => user.email === selectedUserEmail.value) + ); + + return { environment, users, projectId, selectedUserEmail, selectedUser }; +} + +const ExtensionState = + createContext>(null); + +/** Global extension state, this should live high in the react-tree to minimize superfluous renders */ +export function ExtensionStateProvider({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const state = useMemo(() => createExtensionState(), []); + + useEffect(() => { + broker.send("getInitialData"); + }, []); + + useBrokerListener("notifyEnv", ({ env }) => { + state.environment.value = env.isMonospace + ? Environment.IDX + : Environment.VSC; + }); + + useBrokerListener("notifyUsers", ({ users }) => { + state.users.value = users; + }); + + useBrokerListener("notifyUserChanged", ({ user }) => { + state.selectedUserEmail.value = user.email; + }); + + useBrokerListener("notifyProjectChanged", ({ projectId }) => { + state.projectId.value = projectId; + }); + + return ( + {children} + ); +} + +export function useExtensionState() { + return useContext(ExtensionState); +} diff --git a/firebase-vscode/webviews/globals/html-broker.ts b/firebase-vscode/webviews/globals/html-broker.ts new file mode 100644 index 00000000000..59c6d2b9867 --- /dev/null +++ b/firebase-vscode/webviews/globals/html-broker.ts @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import { Broker, createBroker } from "../../common/messaging/broker"; +import { + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, +} from "../../common/messaging/protocol"; +import { webLogger } from "./web-logger"; + +export function useBrokerListener< + MessageT extends keyof ExtensionToWebviewParamsMap +>( + message: Extract, + callback: (value: ExtensionToWebviewParamsMap[MessageT]) => void +) { + useEffect(() => { + return broker.on(message, callback); + }, [message]); +} + +/** Listen to messages, returning the latest sent event */ +export function useBroker( + message: Extract, + options?: { + initialRequest: keyof WebviewToExtensionParamsMap; + } +): ExtensionToWebviewParamsMap[MessageT] | undefined { + const [value, setValue] = useState< + ExtensionToWebviewParamsMap[MessageT] | undefined + >(); + + useEffect(() => { + const unSub = broker.on(message, (value) => { + setValue(value); + }); + + return unSub; + }, [message]); + + useEffect(() => { + if (options?.initialRequest) { + broker.send(options.initialRequest); + } + }, [options?.initialRequest]); + + return value; +} + +export class HtmlBroker extends Broker< + WebviewToExtensionParamsMap, + ExtensionToWebviewParamsMap, + {} +> { + constructor(readonly vscode: any) { + super(); + window.addEventListener("message", (event) => + this.executeListeners(event.data) + ); + + // Log uncaught errors and unhandled rejections + window.addEventListener("error", (event) => { + webLogger.error( + event.error.message, + event.error.stack && "\n", + event.error.stack + ); + }); + window.addEventListener("unhandledrejection", (event) => { + webLogger.error( + "Unhandled rejected promise:", + event.reason, + event.reason.stack && "\n", + event.reason.stack + ); + }); + } + + sendMessage( + command: keyof WebviewToExtensionParamsMap, + data: WebviewToExtensionParamsMap[keyof WebviewToExtensionParamsMap] + ): void { + this.vscode.postMessage({ command, data }); + } +} + +const vscode = (window as any)["acquireVsCodeApi"](); +export const broker = createBroker< + WebviewToExtensionParamsMap, + ExtensionToWebviewParamsMap, + {} +>(new HtmlBroker(vscode)); diff --git a/firebase-vscode/webviews/globals/index.scss b/firebase-vscode/webviews/globals/index.scss new file mode 100644 index 00000000000..f231687635b --- /dev/null +++ b/firebase-vscode/webviews/globals/index.scss @@ -0,0 +1,12 @@ +@import "./tokens.scss"; +@import "./vscode.scss"; + +body { + background-color: transparent; + cursor: default; +} + +:global #root { + display: flex; + flex-direction: column; +} diff --git a/firebase-vscode/webviews/globals/tokens.scss b/firebase-vscode/webviews/globals/tokens.scss new file mode 100644 index 00000000000..0badec1f079 --- /dev/null +++ b/firebase-vscode/webviews/globals/tokens.scss @@ -0,0 +1,8 @@ +:root { + --space-xsmall: 2px; + --space-small: 4px; + --space-medium: 8px; + --space-large: 12px; + --space-xlarge: 16px; + --space-xxlarge: 24px; +} diff --git a/firebase-vscode/webviews/globals/ux-text.ts b/firebase-vscode/webviews/globals/ux-text.ts new file mode 100644 index 00000000000..48e1d73ef77 --- /dev/null +++ b/firebase-vscode/webviews/globals/ux-text.ts @@ -0,0 +1,40 @@ +export const TEXT = { + + LOGIN_PROGRESS: "Checking login", + + MONOSPACE_LOGGED_IN: "Using default credentials", + + MONOSPACE_LOGIN_SELECTION_ITEM: "Default credentials", + + VSCE_SERVICE_ACCOUNT_LOGGED_IN: "Logged in with service account", + + VSCE_SERVICE_ACCOUNT_SELECTION_ITEM: "Service account", + + MONOSPACE_LOGIN_FAIL: "Unable to find default credentials", + + GOOGLE_SIGN_IN: "Sign in with Google", + + ADDITIONAL_USER_SIGN_IN: "Sign in another user...", + + SHOW_SERVICE_ACCOUNT: "Show service account email", + + CONSOLE_LINK_DESCRIPTION: "Open in Firebase console", + + CONNECT_FIREBASE_PROJECT: "Connect a Firebase project", + + DEPLOYING_IN_PROGRESS: "Deploying...", + + DEPLOYING_PROGRESS_FRAMEWORK: "Deploying... this may take a few minutes.", + + DEPLOY_FDC_ENABLED: "Deploy to production", + + DEPLOY_FDC_DISABLED: "Not connected to production", + + DEPLOY_FDC_DESCRIPTION: + "Deploy schema and operations to your production instance.", + + CONNECT_TO_INSTANCE: "Connect to instance", + + CONNECT_TO_INSTANCE_DESCRIPTION: + "Connect to the emulator or a production instance.", +}; diff --git a/firebase-vscode/webviews/globals/vscode.scss b/firebase-vscode/webviews/globals/vscode.scss new file mode 100644 index 00000000000..7533bed4747 --- /dev/null +++ b/firebase-vscode/webviews/globals/vscode.scss @@ -0,0 +1,38 @@ +:root { + --container-padding: 20px; + --vscode-line-height: 140%; +} + +body { + margin: 0; + padding: 0 var(--container-padding); + color: var(--vscode-foreground); + font-size: var(--vscode-font-size); + line-height: var(--vscode-line-height); + font-weight: var(--vscode-font-weight); + font-family: var(--vscode-font-family); + background-color: var(--vscode-editor-background); +} + +ol, +ul { + padding-left: var(--container-padding); +} + +*:focus { + outline-color: var(--vscode-focusBorder) !important; +} + +a { + color: var(--vscode-textLink-foreground); +} + +a:hover, +a:active { + color: var(--vscode-textLink-activeForeground); +} + +code { + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); +} diff --git a/firebase-vscode/webviews/globals/web-logger.ts b/firebase-vscode/webviews/globals/web-logger.ts new file mode 100644 index 00000000000..c8f98188e52 --- /dev/null +++ b/firebase-vscode/webviews/globals/web-logger.ts @@ -0,0 +1,18 @@ +import { broker } from "./html-broker"; + +type Level = "debug" | "info" | "error"; +const levels: Level[] = ["debug", "info", "error"]; + +type WebLogger = Record void>; + +const tempObject = {}; + +for (const level of levels) { + tempObject[level] = (...args: string[]) => + broker.send("writeLog", { level, args }); +} + +// Recast it now that it's populated. +const webLogger = tempObject as WebLogger; + +export { webLogger }; diff --git a/firebase-vscode/webviews/sidebar.entry.scss b/firebase-vscode/webviews/sidebar.entry.scss new file mode 100644 index 00000000000..462fa84fd36 --- /dev/null +++ b/firebase-vscode/webviews/sidebar.entry.scss @@ -0,0 +1,29 @@ +@import "./globals/index.scss"; + +a:not(:hover):not(:focus) { + text-decoration: none; +} + +.fullWidth { + width: 100%; +} + +.integrationStatus { + padding-left: 28px; + position: relative; + + &-label { + line-height: 16px; + } + + &-icon { + position: absolute; + left: 0; + top: 0; + } + + &-loading { + width: 16px; + height: 16px; + } +} \ No newline at end of file diff --git a/firebase-vscode/webviews/sidebar.entry.tsx b/firebase-vscode/webviews/sidebar.entry.tsx new file mode 100644 index 00000000000..cd4b91ee6d2 --- /dev/null +++ b/firebase-vscode/webviews/sidebar.entry.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { SidebarApp } from "./SidebarApp"; +import { App } from "./globals/app"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/firebase-vscode/webviews/tsconfig.json b/firebase-vscode/webviews/tsconfig.json new file mode 100644 index 00000000000..4e0f0928ab2 --- /dev/null +++ b/firebase-vscode/webviews/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "es2020", + "moduleResolution": "Node", + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "esModuleInterop": true, + "jsx": "react", + "sourceMap": true, + "rootDirs": ["./", "../../src", "../common"], + "strict": false /* enable all strict type-checking options */ + }, + "include": ["../webviews/**/*", "../common/**/*"] +} diff --git a/mockdata/function_source_v1.txt b/mockdata/function_source_v1.txt new file mode 100644 index 00000000000..1ff2ebce21e --- /dev/null +++ b/mockdata/function_source_v1.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ac lectus dolor. Fusce luctus non leo ac tempor. Donec interdum magna eget erat aliquam rutrum. Fusce ultricies ut velit eu ullamcorper. Nullam mattis risus pretium, euismod sem id, viverra ligula. Nullam volutpat purus a metus bibendum, et ultrices urna accumsan. Nunc accumsan, nisl ut tristique lacinia, quam dolor lobortis dui, dignissim porta enim lectus at erat. diff --git a/mockdata/function_source_v2.txt b/mockdata/function_source_v2.txt new file mode 100644 index 00000000000..e09e240e82b --- /dev/null +++ b/mockdata/function_source_v2.txt @@ -0,0 +1 @@ +Nullam nisi leo, aliquam eget scelerisque ac, suscipit et nisi. Donec vestibulum sollicitudin nisi, eleifend elementum massa auctor quis. Proin vulputate nisi molestie suscipit aliquam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque mattis metus quis felis consectetur, in venenatis metus consequat. Maecenas a arcu in nulla faucibus tempor. Duis in odio tristique, faucibus eros non, feugiat velit. Maecenas egestas nisl sed diam viverra, vitae mattis ante sagittis. Nunc interdum congue aliquam. Suspendisse vel euismod eros. Fusce et porta augue. Sed at interdum ex, ut dapibus felis. Sed augue nulla, malesuada sed suscipit vel, varius ac diam. diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json new file mode 100644 index 00000000000..2233f95c1b7 --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,36210 @@ +{ + "name": "firebase-tools", + "version": "13.12.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "firebase-tools", + "version": "13.12.0", + "license": "MIT", + "dependencies": { + "@google-cloud/cloud-sql-connector": "^1.2.3", + "@google-cloud/pubsub": "^4.4.0", + "abort-controller": "^3.0.0", + "ajv": "^6.12.6", + "archiver": "^7.0.0", + "async-lock": "1.3.2", + "body-parser": "^1.19.0", + "chokidar": "^3.0.2", + "cjson": "^0.3.1", + "cli-table": "0.3.11", + "colorette": "^2.0.19", + "commander": "^4.0.1", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^5.1.3", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "deep-equal-in-any-order": "^2.0.6", + "exegesis": "^4.1.2", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "fuzzy": "^0.1.3", + "gaxios": "^6.1.1", + "glob": "^10.4.1", + "google-auth-library": "^9.7.0", + "inquirer": "^8.2.6", + "inquirer-autocomplete-prompt": "^2.0.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "marked": "^4.0.14", + "marked-terminal": "^5.1.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "morgan": "^1.10.0", + "node-fetch": "^2.6.7", + "open": "^6.3.0", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "pg": "^8.11.3", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^6.3.0", + "retry": "^0.13.1", + "rimraf": "^5.0.0", + "semver": "^7.5.2", + "sql-formatter": "^15.3.0", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.3", + "triple-beam": "^1.3.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", + "uuid": "^8.3.2", + "winston": "^3.0.0", + "winston-transport": "^4.4.0", + "ws": "^7.2.3", + "yaml": "^2.4.1" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "devDependencies": { + "@angular-devkit/architect": "^0.1402.2", + "@angular-devkit/core": "^14.2.2", + "@google/events": "^5.1.1", + "@types/archiver": "^6.0.0", + "@types/async-lock": "^1.1.5", + "@types/body-parser": "^1.17.0", + "@types/chai": "^4.3.0", + "@types/chai-as-promised": "^7.1.4", + "@types/cjson": "^0.5.0", + "@types/cli-table": "^0.3.0", + "@types/configstore": "^4.0.0", + "@types/cors": "^2.8.10", + "@types/cross-spawn": "^6.0.1", + "@types/deep-equal-in-any-order": "^1.0.3", + "@types/express": "^4.17.0", + "@types/express-serve-static-core": "^4.17.8", + "@types/fs-extra": "^9.0.13", + "@types/html-escaper": "^3.0.0", + "@types/inquirer": "^8.1.3", + "@types/inquirer-autocomplete-prompt": "^2.0.2", + "@types/js-yaml": "^3.12.2", + "@types/jsonwebtoken": "^9.0.5", + "@types/libsodium-wrappers": "^0.7.9", + "@types/lodash": "^4.14.149", + "@types/marked": "^4.0.3", + "@types/marked-terminal": "^3.1.3", + "@types/mocha": "^9.0.0", + "@types/multer": "^1.4.3", + "@types/node": "^18.19.1", + "@types/node-fetch": "^2.5.12", + "@types/pg": "^8.11.2", + "@types/progress": "^2.0.3", + "@types/react": "^18.2.58", + "@types/react-dom": "^18.2.19", + "@types/retry": "^0.12.1", + "@types/semver": "^6.0.0", + "@types/sinon": "^9.0.10", + "@types/sinon-chai": "^3.2.2", + "@types/stream-json": "^1.7.2", + "@types/supertest": "^2.0.12", + "@types/swagger2openapi": "^7.0.0", + "@types/tar": "^6.1.1", + "@types/tcp-port-used": "^1.0.1", + "@types/tmp": "^0.2.3", + "@types/triple-beam": "^1.3.0", + "@types/universal-analytics": "^0.4.5", + "@types/update-notifier": "^5.1.0", + "@types/uuid": "^8.3.1", + "@types/ws": "^7.2.3", + "@typescript-eslint/eslint-plugin": "^5.9.0", + "@typescript-eslint/parser": "^5.9.0", + "astro": "^2.2.3", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "eslint": "^8.56.0", + "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^48.0.1", + "eslint-plugin-prettier": "^5.1.3", + "firebase": "^9.16.0", + "firebase-admin": "^11.5.0", + "firebase-functions": "^4.3.1", + "google-discovery-to-swagger": "^2.1.0", + "googleapis": "^105.0.0", + "mocha": "^9.1.3", + "next": "^14.1.0", + "nock": "^13.0.5", + "node-mocks-http": "^1.11.0", + "nyc": "^15.1.0", + "openapi-merge": "^1.0.23", + "openapi-typescript": "^4.5.0", + "prettier": "^3.2.4", + "proxy": "^1.0.2", + "puppeteer": "^19.0.0", + "sinon": "^9.2.3", + "sinon-chai": "^3.6.0", + "source-map-support": "^0.5.9", + "supertest": "^6.2.3", + "swagger2openapi": "^7.0.8", + "ts-node": "^10.4.0", + "typescript": "^4.5.4", + "typescript-json-schema": "^0.50.1", + "vite": "^4.2.1" + }, + "engines": { + "node": ">=18.0.0 || >=20.0.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1402.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.2.tgz", + "integrity": "sha512-ICcK7OKViMhLkj4btnH/8nv0wjxuKchT/LDN6jfb9gUYUuoon190q0/L/U6ORDwvmjD6sUTurStzOxjuiS0KIg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "14.2.2", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.2.tgz", + "integrity": "sha512-ofDhTmJqoAkmkJP0duwUaCxDBMxPlc+AWYwgs3rKKZeJBb0d+tchEXHXevD5bYbbRfXtnwM+Vye2XYHhA4nWAA==", + "dev": true, + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", + "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@astrojs/compiler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-1.3.1.tgz", + "integrity": "sha512-xV/3r+Hrfpr4ECfJjRjeaMkJvU73KiOADowHjhkqidfNPVAWPzbqw1KePXuMK1TjzMvoAVE7E163oqfH3lDwSw==", + "dev": true + }, + "node_modules/@astrojs/language-server": { + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-0.28.3.tgz", + "integrity": "sha512-fPovAX/X46eE2w03jNRMpQ7W9m2mAvNt4Ay65lD9wl1Z5vIQYxlg7Enp9qP225muTr4jSVB5QiLumFJmZMAaVA==", + "dev": true, + "dependencies": { + "@vscode/emmet-helper": "^2.8.4", + "events": "^3.3.0", + "prettier": "^2.7.1", + "prettier-plugin-astro": "^0.7.0", + "source-map": "^0.7.3", + "vscode-css-languageservice": "^6.0.1", + "vscode-html-languageservice": "^5.0.0", + "vscode-languageserver": "^8.0.1", + "vscode-languageserver-protocol": "^3.17.1", + "vscode-languageserver-textdocument": "^1.0.4", + "vscode-languageserver-types": "^3.17.1", + "vscode-uri": "^3.0.3" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + } + }, + "node_modules/@astrojs/language-server/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@astrojs/language-server/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-2.1.3.tgz", + "integrity": "sha512-Di8Qbit9p7L7eqKklAJmiW9nVD+XMsNHpaNzCLduWjOonDu9fVgEzdjeDrTVCDtgrvkfhpAekuNXrp5+w4F91g==", + "dev": true, + "dependencies": { + "@astrojs/prism": "^2.1.0", + "github-slugger": "^1.4.0", + "import-meta-resolve": "^2.1.0", + "rehype-raw": "^6.1.1", + "rehype-stringify": "^9.0.3", + "remark-gfm": "^3.0.1", + "remark-parse": "^10.0.1", + "remark-rehype": "^10.1.0", + "remark-smartypants": "^2.0.0", + "shiki": "^0.11.1", + "unified": "^10.1.2", + "unist-util-visit": "^4.1.0", + "vfile": "^5.3.2" + }, + "peerDependencies": { + "astro": "^2.2.0" + } + }, + "node_modules/@astrojs/markdown-remark/node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "dev": true + }, + "node_modules/@astrojs/prism": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-2.1.1.tgz", + "integrity": "sha512-Gnwnlb1lGJzCQEg89r4/WqgfCGPNFC7Kuh2D/k289Cbdi/2PD7Lrdstz86y1itDvcb2ijiRqjqWnJ5rsfu/QOA==", + "dev": true, + "dependencies": { + "prismjs": "^1.28.0" + }, + "engines": { + "node": ">=16.12.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-2.1.0.tgz", + "integrity": "sha512-P3gXNNOkRJM8zpnasNoi5kXp3LnFt0smlOSUXhkynfJpTJMIDrcMbKpNORN0OYbqpKt9JPdgRN7nsnGWpbH1ww==", + "dev": true, + "dependencies": { + "ci-info": "^3.3.1", + "debug": "^4.3.4", + "dlv": "^1.1.3", + "dset": "^3.1.2", + "is-docker": "^3.0.0", + "is-wsl": "^2.2.0", + "undici": "^5.20.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": ">=16.12.0" + } + }, + "node_modules/@astrojs/telemetry/node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/@astrojs/telemetry/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@astrojs/telemetry/node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@astrojs/telemetry/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@astrojs/webapi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/webapi/-/webapi-2.1.0.tgz", + "integrity": "sha512-sbF44s/uU33jAdefzKzXZaENPeXR0sR3ptLs+1xp9xf5zIBhedH2AfaFB5qTEv9q5udUVoKxubZGT3G1nWs6rA==", + "dev": true, + "dependencies": { + "undici": "5.20.0" + } + }, + "node_modules/@astrojs/webapi/node_modules/undici": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", + "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", + "dev": true, + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.18" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.0.tgz", + "integrity": "sha512-y5rqgTTPTmaF5e2nVhOxw+Ur9HDJLsWb6U/KpgUzRZEdPfE6VOubXBKLdbcUTijzRptednSBDQbYZBOSqJxpJw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.0.tgz", + "integrity": "sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.0", + "@babel/helper-compilation-targets": "^7.19.0", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", + "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.19.0", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.0.tgz", + "integrity": "sha512-Ai5bNWXIvwDvWM7njqsG3feMlL9hCVQsPYXodsZyLwshYkZVJt59Gftau4VrE8S9IT9asd2uSP1hG6wCNw+sXA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.19.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", + "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", + "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", + "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz", + "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz", + "integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.0", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.19.1", + "@babel/types": "^7.19.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.1.tgz", + "integrity": "sha512-QXgYlXZGprqb6aCBJPPWVBN/Jb69khJF73GGJkOk//PoMgSbPGuaHn1hCRolctnzlBHjCIC6Om97Pw46/1A23g==", + "dev": true, + "dependencies": { + "@emmetio/scanner": "^1.0.2" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.6.tgz", + "integrity": "sha512-bvuPogt0OvwcILRg+ZD/oej1H72xwOhUDPWOmhCWLJrZZ8bMTazsWnvw8a8noaaVqUhOE9PsC0tYgGVv5N7fsw==", + "dev": true, + "dependencies": { + "@emmetio/scanner": "^1.0.2" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.2.tgz", + "integrity": "sha512-1ESCGgXRgn1r29hRmz8K0G4Ywr5jDWezMgRnICComBCWmg3znLWU8+tmakuM1og1Vn4W/sauvlABl/oq2pve8w==", + "dev": true + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.42.0.tgz", + "integrity": "sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==", + "dev": true, + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz", + "integrity": "sha512-baLqRpLe4JnKrUXLJChoTN0iXZH7El/mu58GE3WIA6/H834k0XWvLRmGLG8y8arTRS9hJJibPnF0tiGhmWeZgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.16.tgz", + "integrity": "sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.16.tgz", + "integrity": "sha512-G4wfHhrrz99XJgHnzFvB4UwwPxAWZaZBOFXh+JH1Duf1I4vIVfuYY9uVLpx4eiV2D/Jix8LJY+TAdZ3i40tDow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.16.tgz", + "integrity": "sha512-/Ofw8UXZxuzTLsNFmz1+lmarQI6ztMZ9XktvXedTbt3SNWDn0+ODTwxExLYQ/Hod91EZB4vZPQJLoqLF0jvEzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.16.tgz", + "integrity": "sha512-SzBQtCV3Pdc9kyizh36Ol+dNVhkDyIrGb/JXZqFq8WL37LIyrXU0gUpADcNV311sCOhvY+f2ivMhb5Tuv8nMOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.16.tgz", + "integrity": "sha512-ZqftdfS1UlLiH1DnS2u3It7l4Bc3AskKeu+paJSfk7RNOMrOxmeFDhLTMQqMxycP1C3oj8vgkAT6xfAuq7ZPRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.16.tgz", + "integrity": "sha512-rHV6zNWW1tjgsu0dKQTX9L0ByiJHHLvQKrWtnz8r0YYJI27FU3Xu48gpK2IBj1uCSYhJ+pEk6Y0Um7U3rIvV8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.16.tgz", + "integrity": "sha512-n4O8oVxbn7nl4+m+ISb0a68/lcJClIbaGAoXwqeubj/D1/oMMuaAXmJVfFlRjJLu/ZvHkxoiFJnmbfp4n8cdSw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.16.tgz", + "integrity": "sha512-8yoZhGkU6aHu38WpaM4HrRLTFc7/VVD9Q2SvPcmIQIipQt2I/GMTZNdEHXoypbbGao5kggLcxg0iBKjo0SQYKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.16.tgz", + "integrity": "sha512-9ZBjlkdaVYxPNO8a7OmzDbOH9FMQ1a58j7Xb21UfRU29KcEEU3VTHk+Cvrft/BNv0gpWJMiiZ/f4w0TqSP0gLA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.16.tgz", + "integrity": "sha512-TIZTRojVBBzdgChY3UOG7BlPhqJz08AL7jdgeeu+kiObWMFzGnQD7BgBBkWRwOtKR1i2TNlO7YK6m4zxVjjPRQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.16.tgz", + "integrity": "sha512-UPeRuFKCCJYpBbIdczKyHLAIU31GEm0dZl1eMrdYeXDH+SJZh/i+2cAmD3A1Wip9pIc5Sc6Kc5cFUrPXtR0XHA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.16.tgz", + "integrity": "sha512-io6yShgIEgVUhExJejJ21xvO5QtrbiSeI7vYUnr7l+v/O9t6IowyhdiYnyivX2X5ysOVHAuyHW+Wyi7DNhdw6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.16.tgz", + "integrity": "sha512-WhlGeAHNbSdG/I2gqX2RK2gfgSNwyJuCiFHMc8s3GNEMMHUI109+VMBfhVqRb0ZGzEeRiibi8dItR3ws3Lk+cA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.16.tgz", + "integrity": "sha512-gHRReYsJtViir63bXKoFaQ4pgTyah4ruiMRQ6im9YZuv+gp3UFJkNTY4sFA73YDynmXZA6hi45en4BGhNOJUsw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.16.tgz", + "integrity": "sha512-mfiiBkxEbUHvi+v0P+TS7UnA9TeGXR48aK4XHkTj0ZwOijxexgMF01UDFaBX7Q6CQsB0d+MFNv9IiXbIHTNd4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.16.tgz", + "integrity": "sha512-n8zK1YRDGLRZfVcswcDMDM0j2xKYLNXqei217a4GyBxHIuPMGrrVuJ+Ijfpr0Kufcm7C1k/qaIrGy6eG7wvgmA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.16.tgz", + "integrity": "sha512-lEEfkfsUbo0xC47eSTBqsItXDSzwzwhKUSsVaVjVji07t8+6KA5INp2rN890dHZeueXJAI8q0tEIfbwVRYf6Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.16.tgz", + "integrity": "sha512-jlRjsuvG1fgGwnE8Afs7xYDnGz0dBgTNZfgCK6TlvPH3Z13/P5pi6I57vyLE8qZYLrGVtwcm9UbUx1/mZ8Ukag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.16.tgz", + "integrity": "sha512-TzoU2qwVe2boOHl/3KNBUv2PNUc38U0TNnzqOAcgPiD/EZxT2s736xfC2dYQbszAwo4MKzzwBV0iHjhfjxMimg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.16.tgz", + "integrity": "sha512-B8b7W+oo2yb/3xmwk9Vc99hC9bNolvqjaTZYEfMQhzdpBsjTvZBlXQ/teUE55Ww6sg//wlcDjOaqldOKyigWdA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.16.tgz", + "integrity": "sha512-xJ7OH/nanouJO9pf03YsL9NAFQBHd8AqfrQd7Pf5laGyyTt/gToul6QYOA/i5i/q8y9iaM5DQFNTgpi995VkOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.0.0-rc.9.tgz", + "integrity": "sha512-dGGHpb61hLwifAu7sotuHFDBw6GTdpG8aKC0fsK17EuTzMRvUrH7lEAr6LTJ+sx3AZYed9yZ77rltVDHyg2hRg==", + "dev": true + }, + "node_modules/@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "dev": true, + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dev": true, + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==", + "dev": true + }, + "node_modules/@firebase/analytics/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "dev": true, + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==", + "dev": true + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==", + "dev": true + }, + "node_modules/@firebase/app-check/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "dev": true, + "dependencies": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "dev": true + }, + "node_modules/@firebase/app/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "dev": true, + "dependencies": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==", + "dev": true + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dev": true, + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/component/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "dev": true, + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "dev": true, + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/database/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "dev": true, + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==", + "dev": true + }, + "node_modules/@firebase/functions/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "dev": true + }, + "node_modules/@firebase/installations/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/logger/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==", + "dev": true + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "dev": true + }, + "node_modules/@firebase/messaging/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==", + "dev": true + }, + "node_modules/@firebase/performance/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==", + "dev": true + }, + "node_modules/@firebase/remote-config/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==", + "dev": true + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/@google-cloud/cloud-sql-connector": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@google-cloud/cloud-sql-connector/-/cloud-sql-connector-1.2.3.tgz", + "integrity": "sha512-Johb/LuBv/s0EaoaoeliQCxdjuUN0of6PjdDhBxE7wEeyabli/4wtHOQChm6xtSA/i65ziBrSwSkBnnyHv/yIA==", + "dependencies": { + "@googleapis/sqladmin": "^14.0.0", + "gaxios": "^6.1.1", + "google-auth-library": "^9.2.0", + "p-throttle": "^5.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "dev": true, + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/firestore/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true, + "optional": true + }, + "node_modules/@google-cloud/firestore/node_modules/protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "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/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "dev": true, + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/precise-date": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz", + "integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=12.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==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/pubsub": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.4.0.tgz", + "integrity": "sha512-1eiiAZUFhxcOqKPVwZarc3ghXuhoc3S7z5BgNrxqdirJ/MYr3IjQVTA7Lq2dAAsDuWms1LBN897rbnEGW9PpfA==", + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/precise-date": "^4.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "@opentelemetry/api": "^1.6.0", + "@opentelemetry/semantic-conventions": "~1.21.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^9.3.0", + "google-gax": "^4.3.1", + "heap-js": "^2.2.0", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/pubsub/node_modules/@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/pubsub/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==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/pubsub/node_modules/@grpc/grpc-js": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.8.tgz", + "integrity": "sha512-vYVqYzHicDqyKB+NQhAc54I1QWCBLCrYG6unqOIcBTHx+7x8C9lcoLj3KVJXs2VB4lUbpWY+Kk9NipcbXYWmvg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@google-cloud/pubsub/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==", + "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/@google-cloud/pubsub/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==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/pubsub/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@google-cloud/pubsub/node_modules/google-gax": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.3.tgz", + "integrity": "sha512-f4F2Y9X4+mqsrJuLZsuTljYuQpcBnQsCt9ScvZpdM8jGjqrcxyJi5JUiqtq0jtpdHVPzyit0N7f5t07e+kH5EA==", + "dependencies": { + "@grpc/grpc-js": "~1.10.3", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/pubsub/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==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@google-cloud/pubsub/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/@google-cloud/pubsub/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@google-cloud/pubsub/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==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google-cloud/pubsub/node_modules/proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/pubsub/node_modules/protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "hasInstallScript": true, + "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/@google-cloud/pubsub/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==", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/pubsub/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==", + "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/@google-cloud/pubsub/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" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@google-cloud/pubsub/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/pubsub/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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/@google-cloud/pubsub/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==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "dev": true, + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "dev": true, + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "dev": true, + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "dev": true, + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/storage/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==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@google-cloud/storage/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@google-cloud/storage/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==", + "dev": true, + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@google/events": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@google/events/-/events-5.1.1.tgz", + "integrity": "sha512-97u6AUfEXo6TxoBAdbziuhSL56+l69WzFahR6eTQE/bSjGPqT1+W4vS7eKaR7r60pGFrZZfqdFZ99uMbns3qgA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@googleapis/sqladmin": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-14.1.0.tgz", + "integrity": "sha512-bNly3YIE+aqHc3HfISfK69er0AbqpUlE3THY1XBgj4OHR3b7shDQJaBnzVJkdyfJCrixPKLpJQa4vS+IeDC2hA==", + "dependencies": { + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@googleapis/sqladmin/node_modules/googleapis-common": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz", + "integrity": "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.0.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@googleapis/sqladmin/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" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "dev": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "dev": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "hasInstallScript": true, + "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/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "dev": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", + "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "dev": true, + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@ljharb/has-package-exports-patterns": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@ljharb/has-package-exports-patterns/-/has-package-exports-patterns-0.0.2.tgz", + "integrity": "sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==", + "dev": true + }, + "node_modules/@next/env": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", + "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==", + "dev": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz", + "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz", + "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz", + "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz", + "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz", + "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz", + "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz", + "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz", + "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz", + "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz", + "integrity": "sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/npm-conf": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-1.0.5.tgz", + "integrity": "sha512-hD8ml183638O3R6/Txrh0L8VzGOrFXgRtRDG4qQC4tONdZ5Z1M+tlUUDUvrjYdmK6G+JTBTeaCLMna11cXzi8A==", + "dependencies": { + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "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==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "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": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "node_modules/@puppeteer/browsers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-0.5.0.tgz", + "integrity": "sha512-Uw6oB7VvmPRLE4iKsjuOh8zgDabhNX67dzo8U/BB0f9527qx+4eeUs+korU98OhG5C4ubg7ufBgVi63XYwS6TQ==", + "dev": true, + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=14.1.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/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==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@puppeteer/browsers/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": 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/@puppeteer/browsers/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", + "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "node_modules/@swc/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.0.tgz", + "integrity": "sha512-0mshAzMvdhL0v3lNMJowzMd8Du0bJf+PUTxhVm4uIb/h8qCDQjFERXj0RGejcDFSL7fJzLI3MzS5WR45KDrrLA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, + "bin": { + "swcx": "run_swcx.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-android-arm-eabi": "1.3.0", + "@swc/core-android-arm64": "1.3.0", + "@swc/core-darwin-arm64": "1.3.0", + "@swc/core-darwin-x64": "1.3.0", + "@swc/core-freebsd-x64": "1.3.0", + "@swc/core-linux-arm-gnueabihf": "1.3.0", + "@swc/core-linux-arm64-gnu": "1.3.0", + "@swc/core-linux-arm64-musl": "1.3.0", + "@swc/core-linux-x64-gnu": "1.3.0", + "@swc/core-linux-x64-musl": "1.3.0", + "@swc/core-win32-arm64-msvc": "1.3.0", + "@swc/core-win32-ia32-msvc": "1.3.0", + "@swc/core-win32-x64-msvc": "1.3.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/helpers/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "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==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/archiver": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz", + "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==", + "dev": true, + "dependencies": { + "@types/readdir-glob": "*" + } + }, + "node_modules/@types/async-lock": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.3.0.tgz", + "integrity": "sha512-Z93wDSYW9aMgPR5t+7ouwTvy91Vp3M0Snh4Pd3tf+caSAq5bXZaGnnH9CDbjrwgmfDkRIX0Dx8GvSDgwuoaxoA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", + "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, + "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==" + }, + "node_modules/@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "dev": true + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/cjson": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@types/cjson/-/cjson-0.5.0.tgz", + "integrity": "sha512-fZdrvfhUxvBDQ5+mksCUvUE+nLXwG416gz+iRdYGDEsQQD5mH0PeLzH0ACuRPbobpVvzKjDHo9VYpCKb1EwLIw==", + "dev": true + }, + "node_modules/@types/cli-table": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.0.tgz", + "integrity": "sha512-QnZUISJJXyhyD6L1e5QwXDV/A5i2W1/gl6D6YMc8u0ncPepbv/B4w3S+izVvtAg60m6h+JP09+Y/0zF2mojlFQ==", + "dev": true + }, + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "node_modules/@types/configstore": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-4.0.0.tgz", + "integrity": "sha512-SvCBBPzOIe/3Tu7jTl2Q8NjITjLmq9m7obzjSyb8PXWWZ31xVK6w4T6v8fOx+lrgQnqk3Yxc00LDolFsSakKCA==", + "dev": true + }, + "node_modules/@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", + "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==", + "dev": true + }, + "node_modules/@types/cross-spawn": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.1.tgz", + "integrity": "sha512-MtN1pDYdI6D6QFDzy39Q+6c9rl2o/xN7aWGe6oZuzqq5N6+YuwFsWiEAv3dNzvzN9YzU+itpN8lBzFpphQKLAw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-equal-in-any-order": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.3.tgz", + "integrity": "sha512-jT0O3hAILDKeKbdWJ9FZLD0Xdfhz7hMvfyFlRWpirjiEVr8G+GZ4kVIzPIqM6x6Rpp93TNPgOAed4XmvcuV6Qg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "node_modules/@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true, + "optional": true + }, + "node_modules/@types/express": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.16.tgz", + "integrity": "sha512-LkKpqRZ7zqXJuvoELakaFYuETHjZkSol8EV6cNnyishutDBCCdv6+dsKPbKkCcIk57qRphOLY5sEgClw1bO3gA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "optional": true, + "dependencies": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/html-escaper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/html-escaper/-/html-escaper-3.0.0.tgz", + "integrity": "sha512-OcJcvP3Yk8mjYwf/IdXZtTE1tb/u0WF0qa29ER07ZHCYUBZXSN29Z1mBS+/96+kNMGTFUAbSz9X+pHmHpZrTCw==", + "dev": true + }, + "node_modules/@types/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QXlzybid60YtAwfgG3cpykptRYUx2KomzNutMlWsQC64J/WG/gQSl+P4w7A21sGN0VIxRVava4rgnT7FQmFCdg==", + "dev": true, + "dependencies": { + "@types/through": "*" + } + }, + "node_modules/@types/inquirer-autocomplete-prompt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-2.0.2.tgz", + "integrity": "sha512-Y7RM1dY3KVg11JnFkaQkTT+2Cgmn9K8De/VtrTT2a5grGIoMfkQuYM5Sss+65oiuqg1h1cTsKHG8pkoPsASdbQ==", + "dev": true, + "dependencies": { + "@types/inquirer": "^8" + } + }, + "node_modules/@types/js-yaml": { + "version": "3.12.10", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.10.tgz", + "integrity": "sha512-/Mtaq/wf+HxXpvhzFYzrzCqNRcA958sW++7JOFC8nPrZcvfi/TrzOaaGbvt27ltJB2NQbHVAg5a1wUCsyMH7NA==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.30.tgz", + "integrity": "sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==", + "dev": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz", + "integrity": "sha512-LisgKLlYQk19baQwjkBZZXdJL0KbeTpdEnrAfz5hQACbklCY0gVFnsKUyjfNWF1UQsCSjw93Sj5jSbiO8RPfdw==", + "dev": true + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "optional": true + }, + "node_modules/@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", + "dev": true + }, + "node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", + "dev": true, + "optional": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/marked": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.8.tgz", + "integrity": "sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==", + "dev": true + }, + "node_modules/@types/marked-terminal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/marked-terminal/-/marked-terminal-3.1.3.tgz", + "integrity": "sha512-dKgOLKlI5zFb2jTbRcyQqbdrHxeU74DCOkVIZtsoB2sc1ctXZ1iB2uxG2jjAuzoLdvwHP065ijN6Q8HecWdWYg==", + "dev": true, + "dependencies": { + "@types/marked": "^3", + "chalk": "^2.4.1" + } + }, + "node_modules/@types/marked-terminal/node_modules/@types/marked": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-3.0.3.tgz", + "integrity": "sha512-ZgAr847Wl68W+B0sWH7F4fDPxTzerLnRuUXjUpp1n4NjGSs8hgPAjAp7NQIXblG34MXTrf5wWkAK8PVJ2LIlVg==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", + "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true, + "optional": true + }, + "node_modules/@types/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "dev": true + }, + "node_modules/@types/minipass": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.1.tgz", + "integrity": "sha512-IKmcvG5RnNUtRoxSsusfYnd7fPl8NCLjLutRDvpqwWUR55XvGfy6GIGQUSsKgT2A8qzMjsWfHZNU7d6gxFgqzQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "node_modules/@types/multer": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz", + "integrity": "sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/nlcst": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.0.tgz", + "integrity": "sha512-3TGCfOcy8R8mMQ4CNSNOe3PG66HttvjcLzCoOpvXvDtfWOTi+uT/rxeOKm/qEwbM4SNe1O/PjdiBK2YcTjU4OQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.1.tgz", + "integrity": "sha512-mZJ9V11gG5Vp0Ox2oERpeFDl+JvCwK24PGy76vVY/UgBtjwJWc5rYBThFxmbnYOm9UPZNm6wEl/sxHt2SU7x9A==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true + }, + "node_modules/@types/pg": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.2.tgz", + "integrity": "sha512-G2Mjygf2jFMU/9hCaTYxJrwdObdcnuQde1gndooZSOHsNSaCehAuwc7EIuSA34Do8Jx2yZ19KtvW8P0j4EuUXw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-bPOsfCZ4tsTlKiBjBhKnM8jpY5nmIll166IPD58D92hR7G7kZDfx5iB9wGF4NfZrdKolebjeAr3GouYkSGoJ/A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.58", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.58.tgz", + "integrity": "sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", + "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "dev": true + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", + "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==", + "dev": true + }, + "node_modules/@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sinon": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.10.tgz", + "integrity": "sha512-/faDC0erR06wMdybwI/uR8wEKV/E83T0k4sepIpB7gXuy2gzx2xiOjmztq6a2Y6rIGJ04D+6UU0VBmWy+4HEMA==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinon-chai": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", + "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", + "dev": true, + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz", + "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==", + "dev": true + }, + "node_modules/@types/stream-chain": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stream-chain/-/stream-chain-2.0.1.tgz", + "integrity": "sha512-D+Id9XpcBpampptkegH7WMsEk6fUdf9LlCIX7UhLydILsqDin4L0QT7ryJR0oycwC7OqohIzdfcMHVZ34ezNGg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stream-json": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/stream-json/-/stream-json-1.7.2.tgz", + "integrity": "sha512-i4LE2aWVb1R3p/Z6S6Sw9kmmOs4Drhg0SybZUyfM499I1c8p7MUKZHs4Sg9jL5eu4mDmcgfQ6eGIG3+rmfUWYw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/stream-chain": "*" + } + }, + "node_modules/@types/superagent": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.3.tgz", + "integrity": "sha512-vy2licJQwOXrTAe+yz9SCyUVXAkMgCeDq9VHzS5CWJyDU1g6CI4xKb4d5sCEmyucjw5sG0y4k2/afS0iv/1D0Q==", + "dev": true, + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", + "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", + "dev": true, + "dependencies": { + "@types/superagent": "*" + } + }, + "node_modules/@types/swagger2openapi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/swagger2openapi/-/swagger2openapi-7.0.0.tgz", + "integrity": "sha512-jbjunFpBQqbYt9JZYPDe1G9TkTVzQ8MqT1z7qMq/f7EZzdoA/G8WCZt8dr5gLkATkaE2n8FX7HlrBUTNyYRAJA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "openapi-types": "^12.1.0" + } + }, + "node_modules/@types/tar": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.1.tgz", + "integrity": "sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA==", + "dev": true, + "dependencies": { + "@types/minipass": "*", + "@types/node": "*" + } + }, + "node_modules/@types/tcp-port-used": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/tcp-port-used/-/tcp-port-used-1.0.1.tgz", + "integrity": "sha512-6pwWTx8oUtWvsiZUCrhrK/53MzKVLnuNSSaZILPy3uMes9QnTrLMar9BDlJArbMOjDcjb3QXFk6Rz8qmmuySZw==", + "dev": true + }, + "node_modules/@types/through": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz", + "integrity": "sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==", + "dev": true + }, + "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==" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-tl34wMtk3q+fSdRSJ+N83f47IyXLXPPuLjHm7cmAx0fE2Wml2TZCQV3FmQdSR5J6UEGV3qafG054e0cVVFCqPA==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", + "dev": true + }, + "node_modules/@types/universal-analytics": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@types/universal-analytics/-/universal-analytics-0.4.5.tgz", + "integrity": "sha512-Opb+Un786PS3te24VtJR/QPmX00P/pXaJQtLQYJklQefP4xP0Ic3mPc2z6SDz97OrITzR+RHTBEwjtNRjZ/nLQ==", + "dev": true + }, + "node_modules/@types/update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-aGY5pH1Q/DcToKXl4MCj1c0uDUB+zSVFDRCI7Q7js5sguzBTqJV/5kJA2awofbtWYF3xnon1TYdZYnFditRPtQ==", + "dev": true, + "dependencies": { + "@types/configstore": "*", + "boxen": "^4.2.0" + } + }, + "node_modules/@types/uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.3.tgz", + "integrity": "sha512-VT/GK7nvDA7lfHy40G3LKM+ICqmdIsBLBHGXcWD97MtqQEjNMX+7Gudo8YGpaSlYdTX7IFThhCE8Jx09HegymQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", + "integrity": "sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.51.0", + "@typescript-eslint/type-utils": "5.51.0", + "@typescript-eslint/utils": "5.51.0", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.51.0.tgz", + "integrity": "sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.51.0", + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/typescript-estree": "5.51.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz", + "integrity": "sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/visitor-keys": "5.51.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz", + "integrity": "sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/utils": "5.51.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.51.0.tgz", + "integrity": "sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz", + "integrity": "sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/visitor-keys": "5.51.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.51.0.tgz", + "integrity": "sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.51.0", + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/typescript-estree": "5.51.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz", + "integrity": "sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.51.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.8.6.tgz", + "integrity": "sha512-IIB8jbiKy37zN8bAIHx59YmnIelY78CGHtThnibD/d3tQOKRY83bYVi9blwmZVUZh6l9nfkYH3tvReaiNxY9EQ==", + "dev": true, + "dependencies": { + "emmet": "^2.3.0", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^2.1.2" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true + }, + "node_modules/@vscode/emmet-helper/node_modules/vscode-uri": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==", + "dev": true + }, + "node_modules/@vscode/l10n": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.11.tgz", + "integrity": "sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "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==", + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/agentkeepalive/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agentkeepalive/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "devOptional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/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==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver-utils/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" + } + ] + }, + "node_modules/archiver-utils/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archiver/node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/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" + } + ] + }, + "node_modules/archiver/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/args/node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/args/node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/astro": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/astro/-/astro-2.2.3.tgz", + "integrity": "sha512-Pd67ZBoYxqeyHCZ0UpdmDZYNgcs7JTwc0NMzUScrH4y2hjSY4S8iwmNUtd9pf65gkxMpEbqfvQj06kLzgi4HZg==", + "dev": true, + "dependencies": { + "@astrojs/compiler": "^1.3.1", + "@astrojs/language-server": "^0.28.3", + "@astrojs/markdown-remark": "^2.1.3", + "@astrojs/telemetry": "^2.1.0", + "@astrojs/webapi": "^2.1.0", + "@babel/core": "^7.18.2", + "@babel/generator": "^7.18.2", + "@babel/parser": "^7.18.4", + "@babel/plugin-transform-react-jsx": "^7.17.12", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.4", + "@types/babel__core": "^7.1.19", + "@types/yargs-parser": "^21.0.0", + "acorn": "^8.8.1", + "boxen": "^6.2.1", + "chokidar": "^3.5.3", + "ci-info": "^3.3.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^0.5.0", + "debug": "^4.3.4", + "deepmerge-ts": "^4.2.2", + "devalue": "^4.2.0", + "diff": "^5.1.0", + "es-module-lexer": "^1.1.0", + "estree-walker": "^3.0.1", + "execa": "^6.1.0", + "fast-glob": "^3.2.11", + "github-slugger": "^2.0.0", + "gray-matter": "^4.0.3", + "html-escaper": "^3.0.3", + "kleur": "^4.1.4", + "magic-string": "^0.27.0", + "mime": "^3.0.0", + "ora": "^6.1.0", + "path-to-regexp": "^6.2.1", + "preferred-pm": "^3.0.3", + "prompts": "^2.4.2", + "rehype": "^12.0.1", + "semver": "^7.3.8", + "server-destroy": "^1.0.1", + "shiki": "^0.11.1", + "slash": "^4.0.0", + "string-width": "^5.1.2", + "strip-ansi": "^7.0.1", + "supports-esm": "^1.0.0", + "tsconfig-resolver": "^3.0.1", + "typescript": "*", + "unist-util-visit": "^4.1.0", + "vfile": "^5.3.2", + "vite": "^4.2.1", + "vitefu": "^0.2.4", + "yargs-parser": "^21.0.1", + "zod": "^3.17.3" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": ">=16.12.0", + "npm": ">=6.14.0" + }, + "peerDependencies": { + "sharp": "^0.31.3" + }, + "peerDependenciesMeta": { + "sharp": { + "optional": true + } + } + }, + "node_modules/astro/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astro/node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/astro/node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/astro/node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/astro/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==", + "dev": true + }, + "node_modules/astro/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/astro/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/astro/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/astro/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/astro/node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true + }, + "node_modules/astro/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/log-symbols/node_modules/chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/astro/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/astro/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/astro/node_modules/ora": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.0.tgz", + "integrity": "sha512-1/D8uRFY0ay2kgBpmAwmSA404w4OoPVhHMqRqtjvrcK/dnzcEZxMJ+V4DUbyICu8IIVRclHcOf5wlD1tMY4GUQ==", + "dev": true, + "dependencies": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/ora/node_modules/chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/astro/node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true + }, + "node_modules/astro/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/astro/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/astro/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/astro/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astro/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-lock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", + "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==" + }, + "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==", + "dev": true, + "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": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/atlassian-openapi": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/atlassian-openapi/-/atlassian-openapi-1.0.15.tgz", + "integrity": "sha512-HzgdBHJ/9jZWZfass5DRJNG4vLxoFl6Zcl3B+8Cp2VSpEH7t0laBGnGtcthvj2h73hq8dzjKtVlG30agBZ4OPw==", + "dev": true, + "dependencies": { + "jsonpointer": "^5.0.0", + "urijs": "^1.18.10" + } + }, + "node_modules/atlassian-openapi/node_modules/jsonpointer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz", + "integrity": "sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "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" + } + ] + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=" + }, + "node_modules/basic-auth-parser": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz", + "integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=", + "dev": true + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "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.11.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/body-parser/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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/boxen/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==", + "dev": true + }, + "node_modules/boxen/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "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": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", + "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", + "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "set-function-length": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001589", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", + "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "optional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/chromium-bidi": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", + "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", + "dev": true, + "dependencies": { + "mitt": "3.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "dependencies": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/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==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/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" + } + ] + }, + "node_modules/compress-commons/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "dependencies": { + "mime-db": ">= 1.40.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "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==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/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" + } + ] + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "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": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "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==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dev": true, + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/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" + } + ] + }, + "node_modules/crc32-stream/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-env": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", + "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "dependencies": { + "cross-spawn": "^6.0.5", + "is-windows": "^1.0.0" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/cross-env/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-env/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "node_modules/csv-parse": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", + "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dev": true, + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-equal-in-any-order": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.6.tgz", + "integrity": "sha512-RfnWHQzph10YrUjvWwhd15Dne8ciSJcZ3U6OD7owPwiVwsdE5IFSoZGg8rlwJD11ES+9H5y8j3fCofviRHOqLQ==", + "dependencies": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=" + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "node_modules/deepmerge-ts": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-4.3.0.tgz", + "integrity": "sha512-if3ZYdkD2dClhnXR5reKtG98cwyaRT1NeugQoAPTTfsOpV9kqyeiBF9Qa5RHjemb3KzD5ulqygv6ED3t5j9eJw==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/define-data-property": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", + "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/degenerator/node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/degenerator/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devalue": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.0.tgz", + "integrity": "sha512-n94yQo4LI3w7erwf84mhRUkUJfhLoCZiLyoOZ/QFsDbcWNZePrLwbQpvZBUG2TNxwV3VjCKPxkiiQA6pe3TrTA==", + "dev": true + }, + "node_modules/devtools-protocol": { + "version": "0.0.1107588", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz", + "integrity": "sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==", + "dev": true + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dset": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", + "integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "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==", + "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": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.256", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz", + "integrity": "sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw==", + "dev": true + }, + "node_modules/emmet": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.2.tgz", + "integrity": "sha512-YgmsMkhUgzhJMgH5noGudfxqrQn1bapvF0y7C1e7A0jWFImsRrrvVslzyZz0919NED/cjFOpVWx7c973V+2S/w==", + "dev": true, + "dependencies": { + "@emmetio/abbreviation": "^2.3.1", + "@emmetio/css-abbreviation": "^2.1.6" + } + }, + "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==" + }, + "node_modules/enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "dependencies": { + "env-variable": "0.0.x" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true, + "optional": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==" + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.16.tgz", + "integrity": "sha512-aeSuUKr9aFVY9Dc8ETVELGgkj4urg5isYx8pLf4wlGgB0vTFjxJQdHnNH6Shmx4vYYrOTLCHtRI5i1XZ9l2Zcg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.16", + "@esbuild/android-arm64": "0.17.16", + "@esbuild/android-x64": "0.17.16", + "@esbuild/darwin-arm64": "0.17.16", + "@esbuild/darwin-x64": "0.17.16", + "@esbuild/freebsd-arm64": "0.17.16", + "@esbuild/freebsd-x64": "0.17.16", + "@esbuild/linux-arm": "0.17.16", + "@esbuild/linux-arm64": "0.17.16", + "@esbuild/linux-ia32": "0.17.16", + "@esbuild/linux-loong64": "0.17.16", + "@esbuild/linux-mips64el": "0.17.16", + "@esbuild/linux-ppc64": "0.17.16", + "@esbuild/linux-riscv64": "0.17.16", + "@esbuild/linux-s390x": "0.17.16", + "@esbuild/linux-x64": "0.17.16", + "@esbuild/netbsd-x64": "0.17.16", + "@esbuild/openbsd-x64": "0.17.16", + "@esbuild/sunos-x64": "0.17.16", + "@esbuild/win32-arm64": "0.17.16", + "@esbuild/win32-ia32": "0.17.16", + "@esbuild/win32-x64": "0.17.16" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "48.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.1.0.tgz", + "integrity": "sha512-g9S8ukmTd1DVcV/xeBYPPXOZ6rc8WJ4yi0+MVxJ1jBOrz5kmxV9gJJQ64ltCqIWFnBChLIhLVx3tbTSarqVyFA==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.42.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.6.0", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/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==", + "dev": true + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/eslint/node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==" + }, + "node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exegesis": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.2.tgz", + "integrity": "sha512-D9ZFTFQ8O5ZRBLZ0HAHqo0Gc3+ts330WimHf0cF7OQZLQ3YqRVfjig5qGvEjheS68m+fMjJSR/wN/Qousg17Dw==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^10.3.10", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^3.1.1", + "promise-breaker": "^6.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/exegesis/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/exegesis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/express/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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/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" + } + ] + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "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==" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "dev": true + }, + "node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/fast-url-parser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-yarn-workspace-root2": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", + "integrity": "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==", + "dev": true, + "dependencies": { + "micromatch": "^4.0.2", + "pkg-dir": "^4.2.0" + } + }, + "node_modules/firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "dev": true, + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-admin": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.5.0.tgz", + "integrity": "sha512-bBdlYtNvXx8yZGdCd00NrfZl1o1A0aXOw5h8q5PwC8RXikOLNXq8vYtSKW44dj8zIaafVP6jFdcUXZem/LMsHA==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.3.0", + "@firebase/database-types": "^0.10.0", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2" + } + }, + "node_modules/firebase-admin/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/firebase-functions": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.3.1.tgz", + "integrity": "sha512-sbitfzHcuWsLD03/EgeIRIfkVGeyGjNo3IEA2z+mDIkK1++LhKLCWwVQXrMqeeATOG04CAp30guAagsNElVlng==", + "dev": true, + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/firebase-functions/node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/firebase-functions/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true + }, + "node_modules/firebase-functions/node_modules/protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "hasInstallScript": true, + "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/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fromentries": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.1.tgz", + "integrity": "sha512-Xu2Qh8yqYuDhQGOhD5iJGninErSfI9A3FrriD3tjUgV5VbJFeH8vfgZ9HnC6jWN80QDVNQK5vmxRAmEAp7Mevw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "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": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true, + "optional": true + }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gaxios": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", + "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gaxios/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/gaxios/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==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "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==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=" + }, + "node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", + "dependencies": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/glob/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/google-auth-library": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.7.0.tgz", + "integrity": "sha512-I/AvzBiUXDzLOy4iIZ2W+Zq33W4lcukQv1nl7C8WUA6SQwyQwUwu3waNmWNAvzds//FG8SZ+DnKnW/2k6mQS8A==", + "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-auth-library/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/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==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-discovery-to-swagger": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/google-discovery-to-swagger/-/google-discovery-to-swagger-2.1.0.tgz", + "integrity": "sha512-MI1gfmWPkuXCp6yH+9rfd8ZG8R1R5OIyY4WlKDTqr2+ere1gt2Ne4DSEu8HM7NkwKpuVCE5TrTRAPfm3ownMUQ==", + "dev": true, + "dependencies": { + "json-schema-compatibility": "^1.1.0", + "jsonpath": "^1.0.2", + "lodash": "^4.17.15", + "mime-db": "^1.21.0", + "mime-lookup": "^0.0.2", + "traverse": "~0.6.6", + "urijs": "^1.17.0" + } + }, + "node_modules/google-discovery-to-swagger/node_modules/traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "dev": true, + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/@grpc/grpc-js": { + "version": "1.8.21", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.21.tgz", + "integrity": "sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg==", + "dev": true, + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "dev": true, + "optional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/google-gax/node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "dev": true, + "optional": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/google-gax/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==", + "dev": true, + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/google-gax/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/google-gax/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==", + "dev": true, + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/google-gax/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==", + "dev": true, + "optional": true + }, + "node_modules/google-gax/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/google-gax/node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "dev": true, + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "dev": true, + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "dev": true, + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-gax/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-gax/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/google-gax/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==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-gax/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-gax/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==", + "dev": true, + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-gax/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-gax/node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "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/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "dev": true, + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/google-gax/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true, + "optional": true + }, + "node_modules/google-gax/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/googleapis": { + "version": "105.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-105.0.0.tgz", + "integrity": "sha512-wH/jU/6QpqwsjTKj4vfKZz97ne7xT7BBbKwzQEwnbsG8iH9Seyw19P+AuLJcxNNrmgblwLqfr3LORg4Okat1BQ==", + "dev": true, + "dependencies": { + "google-auth-library": "^8.0.2", + "googleapis-common": "^6.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.3.tgz", + "integrity": "sha512-Xyb4FsQ6PQDu4tAE/M/ev4yzZhFe2Gc7+rKmuCX2ZGk1ajBKbafsGlVYpmzGqQOT93BRDe8DiTmQb6YSkbICrA==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common/node_modules/gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/googleapis-common/node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "dev": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/googleapis-common/node_modules/google-auth-library": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.5.1.tgz", + "integrity": "sha512-7jNMDRhenfw2HLfL9m0ZP/Jw5hzXygfSprzBdypG3rZ+q2gIUbVC/osrFB7y/Z5dkrUr1mnLoDNlerF+p6VXZA==", + "dev": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/googleapis-common/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common/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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/googleapis-common/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis-common/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==", + "dev": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis-common/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/googleapis/node_modules/gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/googleapis/node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "dev": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/googleapis/node_modules/google-auth-library": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.5.1.tgz", + "integrity": "sha512-7jNMDRhenfw2HLfL9m0ZP/Jw5hzXygfSprzBdypG3rZ+q2gIUbVC/osrFB7y/Z5dkrUr1mnLoDNlerF+p6VXZA==", + "dev": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/googleapis/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis/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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/googleapis/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis/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==", + "dev": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-package-exports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/has-package-exports/-/has-package-exports-1.3.0.tgz", + "integrity": "sha512-e9OeXPQnmPhYoJ63lXC4wWe34TxEGZDZ3OQX9XRqp2VwsfLl3bQBy7VehLnd34g3ef8CmYlBLGqEMKXuz8YazQ==", + "dev": true, + "dependencies": { + "@ljharb/has-package-exports-patterns": "^0.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/heap-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.5.0.tgz", + "integrity": "sha512-kUGoI3p7u6B41z/dp33G6OaL7J4DRqRYwVmeIlwLClx7yaaAy7hoDExnuejTKtuDwfcatGmddHDEOjf6EyIxtQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "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==", + "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-errors/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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "dev": true + }, + "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==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-meta-resolve": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", + "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-2.0.1.tgz", + "integrity": "sha512-jUHrH0btO7j5r8DTQgANf2CBkTZChoVySD8zF/wp5fZCOLIuUbleXhf4ZY5jNBOc1owA3gdfWtfZuppfYBhcUg==", + "dependencies": { + "ansi-escapes": "^4.3.2", + "figures": "^3.2.0", + "picocolors": "^1.0.0", + "run-async": "^2.4.1", + "rxjs": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "inquirer": "^8.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/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==" + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/install-artifact-from-github": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "node_modules/is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-fetch/node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=" + }, + "node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "node_modules/jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "optional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "dev": true, + "optional": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsdoc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/json-ptr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.0.1.tgz", + "integrity": "sha512-hrZ4tElT8huJUH3OwOK+d7F8PRqw09QnGM3Mm3GmqKWDyCCPCG8lGHxXOwQAj0VOxzLirOds07Kz10B5F8M8EA==" + }, + "node_modules/json-schema-compatibility": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-schema-compatibility/-/json-schema-compatibility-1.1.0.tgz", + "integrity": "sha1-GomBd4zaDDgYcpjZmdCJ5Rrygt8=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "dependencies": { + "jsonify": "~0.0.0" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "dev": true, + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jsonpath/node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "dev": true + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "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==" + }, + "node_modules/just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, + "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==", + "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.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", + "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", + "dev": true, + "dependencies": { + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^9.0.0", + "debug": "^4.3.4", + "jose": "^4.10.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "dependencies": { + "colornames": "^1.1.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "dependencies": { + "libsodium": "^0.7.0" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "optional": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/load-yaml-file": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", + "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.13.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/load-yaml-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "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==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/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==" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/logform/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dev": true, + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/make-fetch-happen/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==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", + "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "optional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "optional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "optional": true + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/marked-terminal": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", + "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", + "dependencies": { + "ansi-escapes": "^5.0.0", + "cardinal": "^2.1.1", + "chalk": "^5.0.0", + "cli-table3": "^0.6.1", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", + "integrity": "sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "optional": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.1.0.tgz", + "integrity": "sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", + "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz", + "integrity": "sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==", + "dev": true, + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==", + "dev": true, + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.0.tgz", + "integrity": "sha512-RWYce7j8+c0n7Djzv5NzGEGitNNYO3uj+h/XYMdS/JinH1Go+/Qkomg/rfxExFzYTiydaV6GLeffGO5qcJbMPA==", + "dev": true, + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.5.tgz", + "integrity": "sha512-X0oI5eYYQVARhiNfbETy7BfLSmSilzN1eOuoRnrf9oUNsPRrWOAe9UqSizgw1vNxQBfOwL+n2610S3bYjVNi7w==", + "dev": true, + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz", + "integrity": "sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==", + "dev": true, + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dev": true, + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.4.tgz", + "integrity": "sha512-9XlIUUVnYXHsFF2HZ9jby4h3npfX10S1coXTnV035QGPgrtNYQq3J6IfIvcCIUAJrrqBVi5BqA/LmaOMJqPwMQ==", + "dev": true, + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", + "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", + "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", + "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", + "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", + "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", + "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", + "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", + "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", + "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", + "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", + "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", + "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", + "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", + "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", + "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz", + "integrity": "sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", + "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", + "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", + "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/micromark/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-lookup": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/mime-lookup/-/mime-lookup-0.0.2.tgz", + "integrity": "sha1-o1JdJixC5MraWFmR+FADil1dJB0=", + "dev": true + }, + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "optional": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/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==", + "dev": true + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/next": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz", + "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==", + "dev": true, + "dependencies": { + "@next/env": "14.1.0", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.1.0", + "@next/swc-darwin-x64": "14.1.0", + "@next/swc-linux-arm64-gnu": "14.1.0", + "@next/swc-linux-arm64-musl": "14.1.0", + "@next/swc-linux-x64-gnu": "14.1.0", + "@next/swc-linux-x64-musl": "14.1.0", + "@next/swc-win32-arm64-msvc": "14.1.0", + "@next/swc-win32-ia32-msvc": "14.1.0", + "@next/swc-win32-x64-msvc": "14.1.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node_modules/nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/nlcst-to-string": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz", + "integrity": "sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw==", + "dev": true, + "dependencies": { + "@types/nlcst": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/nock": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz", + "integrity": "sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/nock/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nock/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "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-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "dev": true, + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", + "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-mocks-http": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.11.0.tgz", + "integrity": "sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw==", + "dev": true, + "dependencies": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/node-mocks-http/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha1-271K8SE04uY1wkXvk//Pb2BnOl0=", + "dev": true, + "dependencies": { + "es6-promise": "^3.2.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "optional": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nyc/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==", + "dev": true + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "dev": true, + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "dev": true, + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-linter/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "dev": true, + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver/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==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/oas-resolver/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/oas-resolver/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-resolver/node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": 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/oas-resolver/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "dev": true, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "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==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openapi-merge": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/openapi-merge/-/openapi-merge-1.3.2.tgz", + "integrity": "sha512-qRWBwPMiKIUrAcKW6lstMPKpFEWy32dBbP1UjHH9jlWgw++2BCqOVbsjO5Wa4H1Ll3c4cn+lyi4TinUy8iswzw==", + "dev": true, + "dependencies": { + "atlassian-openapi": "^1.0.8", + "lodash": "^4.17.15", + "ts-is-present": "^1.1.1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.0.tgz", + "integrity": "sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA==", + "dev": true + }, + "node_modules/openapi-typescript": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-4.5.0.tgz", + "integrity": "sha512-++gWZLTKmbZP608JHMerllAs84HzULWfVjfH7otkWBLrKxUvzHMFqI6R4JSW1LoNDZnS4KKiRTZW66Fxyp6z4Q==", + "dev": true, + "dependencies": { + "hosted-git-info": "^3.0.8", + "js-yaml": "^4.1.0", + "kleur": "^4.1.4", + "meow": "^9.0.0", + "mime": "^3.0.0", + "node-fetch": "^2.6.6", + "prettier": "^2.5.1", + "slash": "^3.0.0", + "tiny-glob": "^0.2.9" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "engines": { + "node": ">= 12.0.0", + "npm": ">= 7.0.0" + } + }, + "node_modules/openapi-typescript/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/openapi-typescript/node_modules/hosted-git-info": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openapi-typescript/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/openapi-typescript/node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-typescript/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/openapi-typescript/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openapi-typescript/node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openapi-typescript/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/openapi-typescript/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi3-ts": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", + "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", + "dependencies": { + "yaml": "^2.2.1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/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==" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-throttle": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.1.0.tgz", + "integrity": "sha512-+N+s2g01w1Zch4D0K3OpnPDqLOKmLcQ4BvIFq3JC0K29R28vUOjWpO+OJZBNt8X9i3pFCksZJZ0YXkUGjaFE6g==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-latin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-5.0.1.tgz", + "integrity": "sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==", + "dev": true, + "dependencies": { + "nlcst-to-string": "^3.0.0", + "unist-util-modify-children": "^3.0.0", + "unist-util-visit-children": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/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==" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, + "node_modules/preferred-pm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", + "integrity": "sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0", + "find-yarn-workspace-root2": "1.2.16", + "path-exists": "^4.0.0", + "which-pm": "2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-astro": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.7.2.tgz", + "integrity": "sha512-mmifnkG160BtC727gqoimoxnZT/dwr8ASxpoGGl6EHevhfblSOeu+pwH1LAm5Qu1MynizktztFujHHaijLCkww==", + "dev": true, + "dependencies": { + "@astrojs/compiler": "^0.31.3", + "prettier": "^2.7.1", + "sass-formatter": "^0.7.5", + "synckit": "^0.8.4" + }, + "engines": { + "node": "^14.15.0 || >=16.0.0", + "pnpm": ">=7.14.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-0.31.4.tgz", + "integrity": "sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==", + "dev": true + }, + "node_modules/prettier-plugin-astro/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-breaker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz", + "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/property-information": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", + "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" + }, + "node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "dev": true, + "optional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proto3-json-serializer/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true, + "optional": true + }, + "node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "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/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "dev": true, + "hasInstallScript": true, + "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/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.2.tgz", + "integrity": "sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==", + "dev": true, + "dependencies": { + "args": "5.0.1", + "basic-auth-parser": "0.0.2", + "debug": "^4.1.1" + }, + "bin": { + "proxy": "bin/proxy.js" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/proxy/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer": { + "version": "19.11.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.11.1.tgz", + "integrity": "sha512-39olGaX2djYUdhaQQHDZ0T0GwEp+5f9UB9HmEP0qHfdQHIq0xGQZuAZ5TLnJIc/88SrPLpEflPC+xUqOTv3c5g==", + "deprecated": "< 22.5.0 is no longer supported", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "0.5.0", + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "19.11.1" + } + }, + "node_modules/puppeteer-core": { + "version": "19.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.11.1.tgz", + "integrity": "sha512-qcuC2Uf0Fwdj9wNtaTZ2OvYRraXpAK+puwwVW8ofOhOgLPZyz1c68tsorfIZyCUOpyBisjr+xByu7BMbEYMepA==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "0.5.0", + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1107588", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0" + }, + "engines": { + "node": ">=14.14.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "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==", + "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==", + "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/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/re2": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.18.0.tgz", + "integrity": "sha512-MoCYZlJ9YUgksND9asyNF2/x532daXU/ARp1UeJbQ5flMY6ryKNEhrWt85aw3YluzOJlC3vXpGgK2a1jb0b4GA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.3.1", + "nan": "^2.17.0", + "node-gyp": "^9.3.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "dev": true, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rehype": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz", + "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "rehype-parse": "^8.0.0", + "rehype-stringify": "^9.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", + "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", + "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", + "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.0.0.tgz", + "integrity": "sha512-Rc0VDmr/yhnMQIz8n2ACYXlfw/P/XZev884QU1I5u+5DgJls32o97Vc1RbK3pfumLsJomS2yy8eT4Fxj/2MDVA==", + "dev": true, + "dependencies": { + "retext": "^8.1.0", + "retext-smartypants": "^5.1.0", + "unist-util-visit": "^4.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retext": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-8.1.0.tgz", + "integrity": "sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q==", + "dev": true, + "dependencies": { + "@types/nlcst": "^1.0.0", + "retext-latin": "^3.0.0", + "retext-stringify": "^3.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-3.1.0.tgz", + "integrity": "sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==", + "dev": true, + "dependencies": { + "@types/nlcst": "^1.0.0", + "parse-latin": "^5.0.0", + "unherit": "^3.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-5.2.0.tgz", + "integrity": "sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw==", + "dev": true, + "dependencies": { + "@types/nlcst": "^1.0.0", + "nlcst-to-string": "^3.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-3.1.0.tgz", + "integrity": "sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w==", + "dev": true, + "dependencies": { + "@types/nlcst": "^1.0.0", + "nlcst-to-string": "^3.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", + "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", + "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", + "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", + "dependencies": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==" + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/s.color": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", + "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", + "dev": true + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass-formatter": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.6.tgz", + "integrity": "sha512-hXdxU6PCkiV3XAiSnX+XLqz2ohHoEnVUlrd8LEVMAI80uB1+OTScIkH9n6qQwImZpTye1r1WG1rbGUteHNhoHg==", + "dev": true, + "dependencies": { + "suf-log": "^2.5.3" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "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==", + "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==" + }, + "node_modules/send/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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "devOptional": true + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "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==" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shiki": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.11.1.tgz", + "integrity": "sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-oniguruma": "^1.6.1", + "vscode-textmate": "^6.0.0" + } + }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "dev": true, + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "dev": true + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/sinon": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.3.tgz", + "integrity": "sha512-m+DyAWvqVHZtjnjX/nuShasykFeiZ+nPuEfD4G3gpvKGkXRhkF/6NSt2qN2FjZhfrcHXFzUzI+NLnk+42fnLEw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon-chai": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.6.0.tgz", + "integrity": "sha512-bk2h+0xyKnmvazAnc7HE5esttqmCerSMcBtuB2PS2T4tG6x8woXAxZeJaOJWD+8reXHngnXn0RtIbfEW9OTHFg==", + "dev": true, + "peerDependencies": { + "chai": "^4.0.0", + "sinon": ">=4.0.0 <11.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + }, + "node_modules/sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/sql-formatter": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.0.tgz", + "integrity": "sha512-1aDYVEX+dwOSCkRYns4HEGupRZoaivcsNpU4IzR+MVC+cWFYK9/dce7pr4aId4+ED2iK9PNs3j1Vdf8C+SIvDg==", + "dependencies": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + }, + "bin": { + "sql-formatter": "bin/sql-formatter-cli.cjs" + } + }, + "node_modules/sql-formatter/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "engines": { + "node": "*" + } + }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "dev": true, + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stdin-discarder/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/stdin-discarder/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/stream-chain": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.4.tgz", + "integrity": "sha512-9lsl3YM53V5N/I1C2uJtc3Kavyi3kNYN83VkKb/bMWRk7D9imiFyUPYa0PoZbLohSVOX1mYE9YsmwObZUsth6Q==" + }, + "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==", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-json": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.3.tgz", + "integrity": "sha512-Y6dXn9KKWSwxOqnvHGcdZy1PK+J+7alBwHCeU3W9oRqm4ilLRA0XSPmd1tWwhg7tv9EIxJTMWh7KF15tYelKJg==", + "dependencies": { + "stream-chain": "^2.2.4" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dev": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/suf-log": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", + "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", + "dev": true, + "dependencies": { + "s.color": "0.0.15" + } + }, + "node_modules/superagent": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.3.tgz", + "integrity": "sha512-WA6et4nAvgBCS73lJvv1D0ssI5uk5Gh+TGN/kNe+B608EtcVs/yzfl+OLXTzDs7tOBDIpvgh/WUs1K2OK1zTeQ==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "^2.5.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "dependencies": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/superstatic/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/superstatic/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/superstatic/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/supertest": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.3.tgz", + "integrity": "sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^7.1.3" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-esm/-/supports-esm-1.0.0.tgz", + "integrity": "sha512-96Am8CDqUaC0I2+C/swJ0yEvM8ZnGn4unoers/LSdE4umhX7mELzqyLzx3HnZAluq5PXIsGMKqa7NkqaeHMPcg==", + "dev": true, + "dependencies": { + "has-package-exports": "^1.1.0" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/swagger2openapi/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==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/swagger2openapi/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger2openapi/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger2openapi/node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": 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/swagger2openapi/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "dev": true, + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/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==", + "dev": true, + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/term-size": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==", + "dev": true + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "node_modules/trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-is-present": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ts-is-present/-/ts-is-present-1.1.5.tgz", + "integrity": "sha512-7cTV1I0C58HusRxMXTgbAIFu54tB+ZqGX/nf4YuePFiz40NHQbQVBgZSws1No/DJYnGf5Mx26PcyLPol01t5DQ==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-resolver": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tsconfig-resolver/-/tsconfig-resolver-3.0.1.tgz", + "integrity": "sha512-ZHqlstlQF449v8glscGRXzL6l2dZvASPCdXJRWG4gHEZlUVx2Jtmr+a2zeVG4LCsKhDXKRj5R3h0C/98UcVAQg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.30", + "@types/resolve": "^1.17.0", + "json5": "^2.1.3", + "resolve": "^1.17.0", + "strip-bom": "^4.0.0", + "type-fest": "^0.13.1" + }, + "funding": { + "url": "https://github.com/sponsors/ifiokjr" + } + }, + "node_modules/tsconfig-resolver/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/typescript-json-schema": { + "version": "0.50.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.50.1.tgz", + "integrity": "sha512-GCof/SDoiTDl0qzPonNEV4CHyCsZEIIf+mZtlrjoD8vURCcEzEfa2deRuxYid8Znp/e27eDR7Cjg8jgGrimBCA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.7", + "@types/node": "^14.14.33", + "glob": "^7.1.6", + "json-stable-stringify": "^1.0.1", + "ts-node": "^9.1.1", + "typescript": "~4.2.3", + "yargs": "^16.2.0" + }, + "bin": { + "typescript-json-schema": "bin/typescript-json-schema" + } + }, + "node_modules/typescript-json-schema/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "node_modules/typescript-json-schema/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/typescript-json-schema/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-json-schema/node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/typescript-json-schema/node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true, + "optional": true + }, + "node_modules/undici": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.21.2.tgz", + "integrity": "sha512-f6pTQ9RF4DQtwoWSaC42P/NKlUjvezVvd9r155ohqkwFNRyBKM3f3pcty3ouusefNRyM25XhIQEbeQ46sZDJfQ==", + "dev": true, + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.18" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unherit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz", + "integrity": "sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "optional": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-3.1.1.tgz", + "integrity": "sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-2.0.2.tgz", + "integrity": "sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/universal-analytics/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/universal-analytics/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", + "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/update-notifier-cjs/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/update-notifier-cjs/node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier-cjs/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier-cjs/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/update-notifier-cjs/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/update-notifier-cjs/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==" + }, + "node_modules/update-notifier-cjs/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier-cjs/node_modules/registry-auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", + "integrity": "sha512-UfxVOj8seK1yaIOiieV4FIP01vfBDLsY0H9sQzi9EbbUdJiuuBjJgLa1DpImXMNPnVkBD4eVxTEXcrZA6kfpJA==", + "dependencies": { + "@pnpm/npm-conf": "^1.0.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/update-notifier-cjs/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier-cjs/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "dev": true + }, + "node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=" + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "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==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.18.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", + "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.2.4.tgz", + "integrity": "sha512-9UG0s3Ss8rbaaPZL1AkGzdjrGY8F+P+Ne9snsrvD9gxltDGhsn8C2dQpqQewHrMW37OvlqJoI8sUU2AWDb+qNw==", + "dev": true, + "dependencies": { + "@vscode/l10n": "^0.0.11", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + } + }, + "node_modules/vscode-html-languageservice": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.0.4.tgz", + "integrity": "sha512-tvrySfpglu4B2rQgWGVO/IL+skvU7kBkQotRlxA7ocSyRXOZUd6GA13XHkxo8LPe07KWjeoBlN1aVGqdfTK4xA==", + "dev": true, + "dependencies": { + "@vscode/l10n": "^0.0.11", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.2", + "vscode-uri": "^3.0.7" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", + "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", + "dev": true, + "dependencies": { + "vscode-languageserver-protocol": "3.17.3" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "dev": true, + "dependencies": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz", + "integrity": "sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==", + "dev": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==", + "dev": true + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-6.0.0.tgz", + "integrity": "sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==", + "dev": true + }, + "node_modules/vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==", + "dev": true + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/which-pm": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.0.0.tgz", + "integrity": "sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==", + "dev": true, + "dependencies": { + "load-yaml-file": "^0.2.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8.15" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "dependencies": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "dependencies": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "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==", + "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/wrap-ansi-cjs": { + "name": "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==", + "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/wrap-ansi-cjs/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/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==" + }, + "node_modules/wrap-ansi/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/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==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "optional": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/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" + } + ] + }, + "node_modules/zip-stream/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@angular-devkit/architect": { + "version": "0.1402.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.2.tgz", + "integrity": "sha512-ICcK7OKViMhLkj4btnH/8nv0wjxuKchT/LDN6jfb9gUYUuoon190q0/L/U6ORDwvmjD6sUTurStzOxjuiS0KIg==", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.2", + "rxjs": "6.6.7" + }, + "dependencies": { + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + } + } + }, + "@angular-devkit/core": { + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.2.tgz", + "integrity": "sha512-ofDhTmJqoAkmkJP0duwUaCxDBMxPlc+AWYwgs3rKKZeJBb0d+tchEXHXevD5bYbbRfXtnwM+Vye2XYHhA4nWAA==", + "dev": true, + "requires": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, + "@apidevtools/json-schema-ref-parser": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", + "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "requires": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "@astrojs/compiler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-1.3.1.tgz", + "integrity": "sha512-xV/3r+Hrfpr4ECfJjRjeaMkJvU73KiOADowHjhkqidfNPVAWPzbqw1KePXuMK1TjzMvoAVE7E163oqfH3lDwSw==", + "dev": true + }, + "@astrojs/language-server": { + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-0.28.3.tgz", + "integrity": "sha512-fPovAX/X46eE2w03jNRMpQ7W9m2mAvNt4Ay65lD9wl1Z5vIQYxlg7Enp9qP225muTr4jSVB5QiLumFJmZMAaVA==", + "dev": true, + "requires": { + "@vscode/emmet-helper": "^2.8.4", + "events": "^3.3.0", + "prettier": "^2.7.1", + "prettier-plugin-astro": "^0.7.0", + "source-map": "^0.7.3", + "vscode-css-languageservice": "^6.0.1", + "vscode-html-languageservice": "^5.0.0", + "vscode-languageserver": "^8.0.1", + "vscode-languageserver-protocol": "^3.17.1", + "vscode-languageserver-textdocument": "^1.0.4", + "vscode-languageserver-types": "^3.17.1", + "vscode-uri": "^3.0.3" + }, + "dependencies": { + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, + "@astrojs/markdown-remark": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-2.1.3.tgz", + "integrity": "sha512-Di8Qbit9p7L7eqKklAJmiW9nVD+XMsNHpaNzCLduWjOonDu9fVgEzdjeDrTVCDtgrvkfhpAekuNXrp5+w4F91g==", + "dev": true, + "requires": { + "@astrojs/prism": "^2.1.0", + "github-slugger": "^1.4.0", + "import-meta-resolve": "^2.1.0", + "rehype-raw": "^6.1.1", + "rehype-stringify": "^9.0.3", + "remark-gfm": "^3.0.1", + "remark-parse": "^10.0.1", + "remark-rehype": "^10.1.0", + "remark-smartypants": "^2.0.0", + "shiki": "^0.11.1", + "unified": "^10.1.2", + "unist-util-visit": "^4.1.0", + "vfile": "^5.3.2" + }, + "dependencies": { + "github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "dev": true + } + } + }, + "@astrojs/prism": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-2.1.1.tgz", + "integrity": "sha512-Gnwnlb1lGJzCQEg89r4/WqgfCGPNFC7Kuh2D/k289Cbdi/2PD7Lrdstz86y1itDvcb2ijiRqjqWnJ5rsfu/QOA==", + "dev": true, + "requires": { + "prismjs": "^1.28.0" + } + }, + "@astrojs/telemetry": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-2.1.0.tgz", + "integrity": "sha512-P3gXNNOkRJM8zpnasNoi5kXp3LnFt0smlOSUXhkynfJpTJMIDrcMbKpNORN0OYbqpKt9JPdgRN7nsnGWpbH1ww==", + "dev": true, + "requires": { + "ci-info": "^3.3.1", + "debug": "^4.3.4", + "dlv": "^1.1.3", + "dset": "^3.1.2", + "is-docker": "^3.0.0", + "is-wsl": "^2.2.0", + "undici": "^5.20.0", + "which-pm-runs": "^1.1.0" + }, + "dependencies": { + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + }, + "dependencies": { + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@astrojs/webapi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/webapi/-/webapi-2.1.0.tgz", + "integrity": "sha512-sbF44s/uU33jAdefzKzXZaENPeXR0sR3ptLs+1xp9xf5zIBhedH2AfaFB5qTEv9q5udUVoKxubZGT3G1nWs6rA==", + "dev": true, + "requires": { + "undici": "5.20.0" + }, + "dependencies": { + "undici": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", + "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", + "dev": true, + "requires": { + "busboy": "^1.6.0" + } + } + } + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.0.tgz", + "integrity": "sha512-y5rqgTTPTmaF5e2nVhOxw+Ur9HDJLsWb6U/KpgUzRZEdPfE6VOubXBKLdbcUTijzRptednSBDQbYZBOSqJxpJw==", + "dev": true + }, + "@babel/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.0.tgz", + "integrity": "sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.0", + "@babel/helper-compilation-targets": "^7.19.0", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz", + "integrity": "sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==", + "dev": true, + "requires": { + "@babel/types": "^7.19.0", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.0.tgz", + "integrity": "sha512-Ai5bNWXIvwDvWM7njqsG3feMlL9hCVQsPYXodsZyLwshYkZVJt59Gftau4VrE8S9IT9asd2uSP1hG6wCNw+sXA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.19.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", + "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", + "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "dev": true + }, + "@babel/plugin-syntax-jsx": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", + "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz", + "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.21.0" + } + }, + "@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + } + }, + "@babel/traverse": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz", + "integrity": "sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.0", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.19.1", + "@babel/types": "^7.19.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@emmetio/abbreviation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.1.tgz", + "integrity": "sha512-QXgYlXZGprqb6aCBJPPWVBN/Jb69khJF73GGJkOk//PoMgSbPGuaHn1hCRolctnzlBHjCIC6Om97Pw46/1A23g==", + "dev": true, + "requires": { + "@emmetio/scanner": "^1.0.2" + } + }, + "@emmetio/css-abbreviation": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.6.tgz", + "integrity": "sha512-bvuPogt0OvwcILRg+ZD/oej1H72xwOhUDPWOmhCWLJrZZ8bMTazsWnvw8a8noaaVqUhOE9PsC0tYgGVv5N7fsw==", + "dev": true, + "requires": { + "@emmetio/scanner": "^1.0.2" + } + }, + "@emmetio/scanner": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.2.tgz", + "integrity": "sha512-1ESCGgXRgn1r29hRmz8K0G4Ywr5jDWezMgRnICComBCWmg3znLWU8+tmakuM1og1Vn4W/sauvlABl/oq2pve8w==", + "dev": true + }, + "@es-joy/jsdoccomment": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.42.0.tgz", + "integrity": "sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==", + "dev": true, + "requires": { + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + } + }, + "@esbuild/android-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz", + "integrity": "sha512-baLqRpLe4JnKrUXLJChoTN0iXZH7El/mu58GE3WIA6/H834k0XWvLRmGLG8y8arTRS9hJJibPnF0tiGhmWeZgw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.16.tgz", + "integrity": "sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.16.tgz", + "integrity": "sha512-G4wfHhrrz99XJgHnzFvB4UwwPxAWZaZBOFXh+JH1Duf1I4vIVfuYY9uVLpx4eiV2D/Jix8LJY+TAdZ3i40tDow==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.16.tgz", + "integrity": "sha512-/Ofw8UXZxuzTLsNFmz1+lmarQI6ztMZ9XktvXedTbt3SNWDn0+ODTwxExLYQ/Hod91EZB4vZPQJLoqLF0jvEzA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.16.tgz", + "integrity": "sha512-SzBQtCV3Pdc9kyizh36Ol+dNVhkDyIrGb/JXZqFq8WL37LIyrXU0gUpADcNV311sCOhvY+f2ivMhb5Tuv8nMOQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.16.tgz", + "integrity": "sha512-ZqftdfS1UlLiH1DnS2u3It7l4Bc3AskKeu+paJSfk7RNOMrOxmeFDhLTMQqMxycP1C3oj8vgkAT6xfAuq7ZPRA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.16.tgz", + "integrity": "sha512-rHV6zNWW1tjgsu0dKQTX9L0ByiJHHLvQKrWtnz8r0YYJI27FU3Xu48gpK2IBj1uCSYhJ+pEk6Y0Um7U3rIvV8g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.16.tgz", + "integrity": "sha512-n4O8oVxbn7nl4+m+ISb0a68/lcJClIbaGAoXwqeubj/D1/oMMuaAXmJVfFlRjJLu/ZvHkxoiFJnmbfp4n8cdSw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.16.tgz", + "integrity": "sha512-8yoZhGkU6aHu38WpaM4HrRLTFc7/VVD9Q2SvPcmIQIipQt2I/GMTZNdEHXoypbbGao5kggLcxg0iBKjo0SQYKA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.16.tgz", + "integrity": "sha512-9ZBjlkdaVYxPNO8a7OmzDbOH9FMQ1a58j7Xb21UfRU29KcEEU3VTHk+Cvrft/BNv0gpWJMiiZ/f4w0TqSP0gLA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.16.tgz", + "integrity": "sha512-TIZTRojVBBzdgChY3UOG7BlPhqJz08AL7jdgeeu+kiObWMFzGnQD7BgBBkWRwOtKR1i2TNlO7YK6m4zxVjjPRQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.16.tgz", + "integrity": "sha512-UPeRuFKCCJYpBbIdczKyHLAIU31GEm0dZl1eMrdYeXDH+SJZh/i+2cAmD3A1Wip9pIc5Sc6Kc5cFUrPXtR0XHA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.16.tgz", + "integrity": "sha512-io6yShgIEgVUhExJejJ21xvO5QtrbiSeI7vYUnr7l+v/O9t6IowyhdiYnyivX2X5ysOVHAuyHW+Wyi7DNhdw6Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.16.tgz", + "integrity": "sha512-WhlGeAHNbSdG/I2gqX2RK2gfgSNwyJuCiFHMc8s3GNEMMHUI109+VMBfhVqRb0ZGzEeRiibi8dItR3ws3Lk+cA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.16.tgz", + "integrity": "sha512-gHRReYsJtViir63bXKoFaQ4pgTyah4ruiMRQ6im9YZuv+gp3UFJkNTY4sFA73YDynmXZA6hi45en4BGhNOJUsw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.16.tgz", + "integrity": "sha512-mfiiBkxEbUHvi+v0P+TS7UnA9TeGXR48aK4XHkTj0ZwOijxexgMF01UDFaBX7Q6CQsB0d+MFNv9IiXbIHTNd4g==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.16.tgz", + "integrity": "sha512-n8zK1YRDGLRZfVcswcDMDM0j2xKYLNXqei217a4GyBxHIuPMGrrVuJ+Ijfpr0Kufcm7C1k/qaIrGy6eG7wvgmA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.16.tgz", + "integrity": "sha512-lEEfkfsUbo0xC47eSTBqsItXDSzwzwhKUSsVaVjVji07t8+6KA5INp2rN890dHZeueXJAI8q0tEIfbwVRYf6Ew==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.16.tgz", + "integrity": "sha512-jlRjsuvG1fgGwnE8Afs7xYDnGz0dBgTNZfgCK6TlvPH3Z13/P5pi6I57vyLE8qZYLrGVtwcm9UbUx1/mZ8Ukag==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.16.tgz", + "integrity": "sha512-TzoU2qwVe2boOHl/3KNBUv2PNUc38U0TNnzqOAcgPiD/EZxT2s736xfC2dYQbszAwo4MKzzwBV0iHjhfjxMimg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.16.tgz", + "integrity": "sha512-B8b7W+oo2yb/3xmwk9Vc99hC9bNolvqjaTZYEfMQhzdpBsjTvZBlXQ/teUE55Ww6sg//wlcDjOaqldOKyigWdA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.16.tgz", + "integrity": "sha512-xJ7OH/nanouJO9pf03YsL9NAFQBHd8AqfrQd7Pf5laGyyTt/gToul6QYOA/i5i/q8y9iaM5DQFNTgpi995VkOg==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true + }, + "@exodus/schemasafe": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.0.0-rc.9.tgz", + "integrity": "sha512-dGGHpb61hLwifAu7sotuHFDBw6GTdpG8aKC0fsK17EuTzMRvUrH7lEAr6LTJ+sx3AZYed9yZ77rltVDHyg2hRg==", + "dev": true + }, + "@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "dev": true, + "requires": { + "text-decoding": "^1.0.0" + } + }, + "@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dev": true, + "requires": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==", + "dev": true + }, + "@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "dev": true, + "requires": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==", + "dev": true + }, + "@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==", + "dev": true + }, + "@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "dev": true, + "requires": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "dev": true + }, + "@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "dev": true, + "requires": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==", + "dev": true + }, + "@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "dev": true, + "requires": {} + }, + "@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dev": true, + "requires": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "dev": true, + "requires": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } + }, + "@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "dev": true, + "requires": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "dev": true, + "requires": {} + }, + "@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "dev": true, + "requires": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==", + "dev": true + }, + "@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "dev": true + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "dev": true, + "requires": {} + }, + "@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } + }, + "@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "dev": true + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==", + "dev": true + }, + "@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==", + "dev": true + }, + "@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==", + "dev": true + }, + "@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "dev": true, + "requires": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "dev": true, + "requires": {} + }, + "@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } + }, + "@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==", + "dev": true + }, + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "@google-cloud/cloud-sql-connector": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@google-cloud/cloud-sql-connector/-/cloud-sql-connector-1.2.3.tgz", + "integrity": "sha512-Johb/LuBv/s0EaoaoeliQCxdjuUN0of6PjdDhBxE7wEeyabli/4wtHOQChm6xtSA/i65ziBrSwSkBnnyHv/yIA==", + "requires": { + "@googleapis/sqladmin": "^14.0.0", + "gaxios": "^6.1.1", + "google-auth-library": "^9.2.0", + "p-throttle": "^5.1.0" + } + }, + "@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "dev": true, + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true, + "optional": true + }, + "protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "optional": true, + "requires": { + "@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" + } + } + } + }, + "@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "dev": true, + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/precise-date": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz", + "integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==" + }, + "@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "dev": true, + "optional": true + }, + "@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==" + }, + "@google-cloud/pubsub": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.4.0.tgz", + "integrity": "sha512-1eiiAZUFhxcOqKPVwZarc3ghXuhoc3S7z5BgNrxqdirJ/MYr3IjQVTA7Lq2dAAsDuWms1LBN897rbnEGW9PpfA==", + "requires": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/precise-date": "^4.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "@opentelemetry/api": "^1.6.0", + "@opentelemetry/semantic-conventions": "~1.21.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^9.3.0", + "google-gax": "^4.3.1", + "heap-js": "^2.2.0", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "dependencies": { + "@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==" + }, + "@grpc/grpc-js": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.8.tgz", + "integrity": "sha512-vYVqYzHicDqyKB+NQhAc54I1QWCBLCrYG6unqOIcBTHx+7x8C9lcoLj3KVJXs2VB4lUbpWY+Kk9NipcbXYWmvg==", + "requires": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + } + }, + "@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==", + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + } + }, + "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==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "google-gax": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.3.tgz", + "integrity": "sha512-f4F2Y9X4+mqsrJuLZsuTljYuQpcBnQsCt9ScvZpdM8jGjqrcxyJi5JUiqtq0jtpdHVPzyit0N7f5t07e+kH5EA==", + "requires": { + "@grpc/grpc-js": "~1.10.3", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + } + }, + "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==", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "requires": { + "protobufjs": "^7.2.5" + } + }, + "protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "requires": { + "@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" + } + }, + "retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "requires": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + } + }, + "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==", + "requires": { + "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" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "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" + } + }, + "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==" + } + } + }, + "@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "dev": true, + "optional": true, + "requires": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "dependencies": { + "@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "dev": true, + "optional": true + }, + "gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "dev": true, + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "dev": true, + "optional": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "dev": true, + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "optional": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "optional": true, + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "optional": true + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "optional": true + } + } + }, + "@google/events": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@google/events/-/events-5.1.1.tgz", + "integrity": "sha512-97u6AUfEXo6TxoBAdbziuhSL56+l69WzFahR6eTQE/bSjGPqT1+W4vS7eKaR7r60pGFrZZfqdFZ99uMbns3qgA==", + "dev": true + }, + "@googleapis/sqladmin": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-14.1.0.tgz", + "integrity": "sha512-bNly3YIE+aqHc3HfISfK69er0AbqpUlE3THY1XBgj4OHR3b7shDQJaBnzVJkdyfJCrixPKLpJQa4vS+IeDC2hA==", + "requires": { + "googleapis-common": "^7.0.0" + }, + "dependencies": { + "googleapis-common": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.0.1.tgz", + "integrity": "sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.0.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } + } + }, + "@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "dev": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "dependencies": { + "@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "dev": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + } + }, + "protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true + } + } + } + } + }, + "@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "dev": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", + "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@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==" + }, + "@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "dev": true, + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "@ljharb/has-package-exports-patterns": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@ljharb/has-package-exports-patterns/-/has-package-exports-patterns-0.0.2.tgz", + "integrity": "sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==", + "dev": true + }, + "@next/env": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", + "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==", + "dev": true + }, + "@next/swc-darwin-arm64": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz", + "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==", + "dev": true, + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz", + "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==", + "dev": true, + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz", + "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==", + "dev": true, + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz", + "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==", + "dev": true, + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz", + "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==", + "dev": true, + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz", + "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==", + "dev": true, + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz", + "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==", + "dev": true, + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz", + "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==", + "dev": true, + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz", + "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==", + "dev": true, + "optional": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "optional": true, + "requires": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + } + }, + "@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "optional": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==" + }, + "@opentelemetry/semantic-conventions": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz", + "integrity": "sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==" + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true + }, + "@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "requires": { + "graceful-fs": "4.2.10" + } + }, + "@pnpm/npm-conf": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-1.0.5.tgz", + "integrity": "sha512-hD8ml183638O3R6/Txrh0L8VzGOrFXgRtRDG4qQC4tONdZ5Z1M+tlUUDUvrjYdmK6G+JTBTeaCLMna11cXzi8A==", + "requires": { + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "@puppeteer/browsers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-0.5.0.tgz", + "integrity": "sha512-Uw6oB7VvmPRLE4iKsjuOh8zgDabhNX67dzo8U/BB0f9527qx+4eeUs+korU98OhG5C4ubg7ufBgVi63XYwS6TQ==", + "dev": true, + "requires": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "dependencies": { + "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==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": true, + "requires": { + "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" + } + }, + "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==", + "dev": true + } + } + }, + "@sinonjs/commons": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", + "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@swc/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.0.tgz", + "integrity": "sha512-0mshAzMvdhL0v3lNMJowzMd8Du0bJf+PUTxhVm4uIb/h8qCDQjFERXj0RGejcDFSL7fJzLI3MzS5WR45KDrrLA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/core-android-arm-eabi": "1.3.0", + "@swc/core-android-arm64": "1.3.0", + "@swc/core-darwin-arm64": "1.3.0", + "@swc/core-darwin-x64": "1.3.0", + "@swc/core-freebsd-x64": "1.3.0", + "@swc/core-linux-arm-gnueabihf": "1.3.0", + "@swc/core-linux-arm64-gnu": "1.3.0", + "@swc/core-linux-arm64-musl": "1.3.0", + "@swc/core-linux-x64-gnu": "1.3.0", + "@swc/core-linux-x64-musl": "1.3.0", + "@swc/core-win32-arm64-msvc": "1.3.0", + "@swc/core-win32-ia32-msvc": "1.3.0", + "@swc/core-win32-x64-msvc": "1.3.0" + } + }, + "@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "dev": true, + "requires": { + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "@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==" + }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "@types/archiver": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz", + "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==", + "dev": true, + "requires": { + "@types/readdir-glob": "*" + } + }, + "@types/async-lock": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.3.0.tgz", + "integrity": "sha512-Z93wDSYW9aMgPR5t+7ouwTvy91Vp3M0Snh4Pd3tf+caSAq5bXZaGnnH9CDbjrwgmfDkRIX0Dx8GvSDgwuoaxoA==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", + "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" + }, + "@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/cjson": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@types/cjson/-/cjson-0.5.0.tgz", + "integrity": "sha512-fZdrvfhUxvBDQ5+mksCUvUE+nLXwG416gz+iRdYGDEsQQD5mH0PeLzH0ACuRPbobpVvzKjDHo9VYpCKb1EwLIw==", + "dev": true + }, + "@types/cli-table": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.0.tgz", + "integrity": "sha512-QnZUISJJXyhyD6L1e5QwXDV/A5i2W1/gl6D6YMc8u0ncPepbv/B4w3S+izVvtAg60m6h+JP09+Y/0zF2mojlFQ==", + "dev": true + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/configstore": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-4.0.0.tgz", + "integrity": "sha512-SvCBBPzOIe/3Tu7jTl2Q8NjITjLmq9m7obzjSyb8PXWWZ31xVK6w4T6v8fOx+lrgQnqk3Yxc00LDolFsSakKCA==", + "dev": true + }, + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "@types/cors": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", + "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==", + "dev": true + }, + "@types/cross-spawn": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.1.tgz", + "integrity": "sha512-MtN1pDYdI6D6QFDzy39Q+6c9rl2o/xN7aWGe6oZuzqq5N6+YuwFsWiEAv3dNzvzN9YzU+itpN8lBzFpphQKLAw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/deep-equal-in-any-order": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.3.tgz", + "integrity": "sha512-jT0O3hAILDKeKbdWJ9FZLD0Xdfhz7hMvfyFlRWpirjiEVr8G+GZ4kVIzPIqM6x6Rpp93TNPgOAed4XmvcuV6Qg==", + "dev": true + }, + "@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true, + "optional": true + }, + "@types/express": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.16.tgz", + "integrity": "sha512-LkKpqRZ7zqXJuvoELakaFYuETHjZkSol8EV6cNnyishutDBCCdv6+dsKPbKkCcIk57qRphOLY5sEgClw1bO3gA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "optional": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, + "@types/html-escaper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/html-escaper/-/html-escaper-3.0.0.tgz", + "integrity": "sha512-OcJcvP3Yk8mjYwf/IdXZtTE1tb/u0WF0qa29ER07ZHCYUBZXSN29Z1mBS+/96+kNMGTFUAbSz9X+pHmHpZrTCw==", + "dev": true + }, + "@types/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QXlzybid60YtAwfgG3cpykptRYUx2KomzNutMlWsQC64J/WG/gQSl+P4w7A21sGN0VIxRVava4rgnT7FQmFCdg==", + "dev": true, + "requires": { + "@types/through": "*" + } + }, + "@types/inquirer-autocomplete-prompt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-2.0.2.tgz", + "integrity": "sha512-Y7RM1dY3KVg11JnFkaQkTT+2Cgmn9K8De/VtrTT2a5grGIoMfkQuYM5Sss+65oiuqg1h1cTsKHG8pkoPsASdbQ==", + "dev": true, + "requires": { + "@types/inquirer": "^8" + } + }, + "@types/js-yaml": { + "version": "3.12.10", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.10.tgz", + "integrity": "sha512-/Mtaq/wf+HxXpvhzFYzrzCqNRcA958sW++7JOFC8nPrZcvfi/TrzOaaGbvt27ltJB2NQbHVAg5a1wUCsyMH7NA==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.30.tgz", + "integrity": "sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==", + "dev": true + }, + "@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/libsodium-wrappers": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz", + "integrity": "sha512-LisgKLlYQk19baQwjkBZZXdJL0KbeTpdEnrAfz5hQACbklCY0gVFnsKUyjfNWF1UQsCSjw93Sj5jSbiO8RPfdw==", + "dev": true + }, + "@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "optional": true + }, + "@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", + "dev": true + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", + "dev": true, + "optional": true, + "requires": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "@types/marked": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.8.tgz", + "integrity": "sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==", + "dev": true + }, + "@types/marked-terminal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/marked-terminal/-/marked-terminal-3.1.3.tgz", + "integrity": "sha512-dKgOLKlI5zFb2jTbRcyQqbdrHxeU74DCOkVIZtsoB2sc1ctXZ1iB2uxG2jjAuzoLdvwHP065ijN6Q8HecWdWYg==", + "dev": true, + "requires": { + "@types/marked": "^3", + "chalk": "^2.4.1" + }, + "dependencies": { + "@types/marked": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-3.0.3.tgz", + "integrity": "sha512-ZgAr847Wl68W+B0sWH7F4fDPxTzerLnRuUXjUpp1n4NjGSs8hgPAjAp7NQIXblG34MXTrf5wWkAK8PVJ2LIlVg==", + "dev": true + } + } + }, + "@types/mdast": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", + "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, + "@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "optional": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true, + "optional": true + }, + "@types/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "dev": true + }, + "@types/minipass": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.1.tgz", + "integrity": "sha512-IKmcvG5RnNUtRoxSsusfYnd7fPl8NCLjLutRDvpqwWUR55XvGfy6GIGQUSsKgT2A8qzMjsWfHZNU7d6gxFgqzQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "@types/multer": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz", + "integrity": "sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/nlcst": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.0.tgz", + "integrity": "sha512-3TGCfOcy8R8mMQ4CNSNOe3PG66HttvjcLzCoOpvXvDtfWOTi+uT/rxeOKm/qEwbM4SNe1O/PjdiBK2YcTjU4OQ==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, + "@types/node": { + "version": "18.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.1.tgz", + "integrity": "sha512-mZJ9V11gG5Vp0Ox2oERpeFDl+JvCwK24PGy76vVY/UgBtjwJWc5rYBThFxmbnYOm9UPZNm6wEl/sxHt2SU7x9A==", + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true + }, + "@types/pg": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.2.tgz", + "integrity": "sha512-G2Mjygf2jFMU/9hCaTYxJrwdObdcnuQde1gndooZSOHsNSaCehAuwc7EIuSA34Do8Jx2yZ19KtvW8P0j4EuUXw==", + "dev": true, + "requires": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + }, + "dependencies": { + "pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "requires": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + } + }, + "postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true + }, + "postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "requires": { + "obuf": "~1.1.2" + } + }, + "postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true + }, + "postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true + } + } + }, + "@types/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-bPOsfCZ4tsTlKiBjBhKnM8jpY5nmIll166IPD58D92hR7G7kZDfx5iB9wGF4NfZrdKolebjeAr3GouYkSGoJ/A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/react": { + "version": "18.2.58", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.58.tgz", + "integrity": "sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", + "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "dev": true + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "@types/semver": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", + "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/sinon": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.10.tgz", + "integrity": "sha512-/faDC0erR06wMdybwI/uR8wEKV/E83T0k4sepIpB7gXuy2gzx2xiOjmztq6a2Y6rIGJ04D+6UU0VBmWy+4HEMA==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinon-chai": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", + "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz", + "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==", + "dev": true + }, + "@types/stream-chain": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stream-chain/-/stream-chain-2.0.1.tgz", + "integrity": "sha512-D+Id9XpcBpampptkegH7WMsEk6fUdf9LlCIX7UhLydILsqDin4L0QT7ryJR0oycwC7OqohIzdfcMHVZ34ezNGg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/stream-json": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/stream-json/-/stream-json-1.7.2.tgz", + "integrity": "sha512-i4LE2aWVb1R3p/Z6S6Sw9kmmOs4Drhg0SybZUyfM499I1c8p7MUKZHs4Sg9jL5eu4mDmcgfQ6eGIG3+rmfUWYw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/stream-chain": "*" + } + }, + "@types/superagent": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.3.tgz", + "integrity": "sha512-vy2licJQwOXrTAe+yz9SCyUVXAkMgCeDq9VHzS5CWJyDU1g6CI4xKb4d5sCEmyucjw5sG0y4k2/afS0iv/1D0Q==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", + "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, + "@types/swagger2openapi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/swagger2openapi/-/swagger2openapi-7.0.0.tgz", + "integrity": "sha512-jbjunFpBQqbYt9JZYPDe1G9TkTVzQ8MqT1z7qMq/f7EZzdoA/G8WCZt8dr5gLkATkaE2n8FX7HlrBUTNyYRAJA==", + "dev": true, + "requires": { + "@types/node": "*", + "openapi-types": "^12.1.0" + } + }, + "@types/tar": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.1.tgz", + "integrity": "sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA==", + "dev": true, + "requires": { + "@types/minipass": "*", + "@types/node": "*" + } + }, + "@types/tcp-port-used": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/tcp-port-used/-/tcp-port-used-1.0.1.tgz", + "integrity": "sha512-6pwWTx8oUtWvsiZUCrhrK/53MzKVLnuNSSaZILPy3uMes9QnTrLMar9BDlJArbMOjDcjb3QXFk6Rz8qmmuySZw==", + "dev": true + }, + "@types/through": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz", + "integrity": "sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==", + "dev": true + }, + "@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==" + }, + "@types/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-tl34wMtk3q+fSdRSJ+N83f47IyXLXPPuLjHm7cmAx0fE2Wml2TZCQV3FmQdSR5J6UEGV3qafG054e0cVVFCqPA==", + "dev": true + }, + "@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", + "dev": true + }, + "@types/universal-analytics": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@types/universal-analytics/-/universal-analytics-0.4.5.tgz", + "integrity": "sha512-Opb+Un786PS3te24VtJR/QPmX00P/pXaJQtLQYJklQefP4xP0Ic3mPc2z6SDz97OrITzR+RHTBEwjtNRjZ/nLQ==", + "dev": true + }, + "@types/update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-aGY5pH1Q/DcToKXl4MCj1c0uDUB+zSVFDRCI7Q7js5sguzBTqJV/5kJA2awofbtWYF3xnon1TYdZYnFditRPtQ==", + "dev": true, + "requires": { + "@types/configstore": "*", + "boxen": "^4.2.0" + } + }, + "@types/uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", + "dev": true + }, + "@types/ws": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.3.tgz", + "integrity": "sha512-VT/GK7nvDA7lfHy40G3LKM+ICqmdIsBLBHGXcWD97MtqQEjNMX+7Gudo8YGpaSlYdTX7IFThhCE8Jx09HegymQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", + "integrity": "sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.51.0", + "@typescript-eslint/type-utils": "5.51.0", + "@typescript-eslint/utils": "5.51.0", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.51.0.tgz", + "integrity": "sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.51.0", + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/typescript-estree": "5.51.0", + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz", + "integrity": "sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/visitor-keys": "5.51.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz", + "integrity": "sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/utils": "5.51.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/types": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.51.0.tgz", + "integrity": "sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz", + "integrity": "sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/visitor-keys": "5.51.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.51.0.tgz", + "integrity": "sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.51.0", + "@typescript-eslint/types": "5.51.0", + "@typescript-eslint/typescript-estree": "5.51.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "dependencies": { + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz", + "integrity": "sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.51.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "@vscode/emmet-helper": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.8.6.tgz", + "integrity": "sha512-IIB8jbiKy37zN8bAIHx59YmnIelY78CGHtThnibD/d3tQOKRY83bYVi9blwmZVUZh6l9nfkYH3tvReaiNxY9EQ==", + "dev": true, + "requires": { + "emmet": "^2.3.0", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^2.1.2" + }, + "dependencies": { + "jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true + }, + "vscode-uri": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==", + "dev": true + } + } + }, + "@vscode/l10n": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.11.tgz", + "integrity": "sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "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==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "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==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "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==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "devOptional": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "requires": { + "string-width": "^4.1.0" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "requires": { + "type-fest": "^1.0.2" + }, + "dependencies": { + "type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==" + } + } + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "requires": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "dependencies": { + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==" + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "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==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "requires": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "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==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true + }, + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "dependencies": { + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + } + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "astro": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/astro/-/astro-2.2.3.tgz", + "integrity": "sha512-Pd67ZBoYxqeyHCZ0UpdmDZYNgcs7JTwc0NMzUScrH4y2hjSY4S8iwmNUtd9pf65gkxMpEbqfvQj06kLzgi4HZg==", + "dev": true, + "requires": { + "@astrojs/compiler": "^1.3.1", + "@astrojs/language-server": "^0.28.3", + "@astrojs/markdown-remark": "^2.1.3", + "@astrojs/telemetry": "^2.1.0", + "@astrojs/webapi": "^2.1.0", + "@babel/core": "^7.18.2", + "@babel/generator": "^7.18.2", + "@babel/parser": "^7.18.4", + "@babel/plugin-transform-react-jsx": "^7.17.12", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.4", + "@types/babel__core": "^7.1.19", + "@types/yargs-parser": "^21.0.0", + "acorn": "^8.8.1", + "boxen": "^6.2.1", + "chokidar": "^3.5.3", + "ci-info": "^3.3.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^0.5.0", + "debug": "^4.3.4", + "deepmerge-ts": "^4.2.2", + "devalue": "^4.2.0", + "diff": "^5.1.0", + "es-module-lexer": "^1.1.0", + "estree-walker": "^3.0.1", + "execa": "^6.1.0", + "fast-glob": "^3.2.11", + "github-slugger": "^2.0.0", + "gray-matter": "^4.0.3", + "html-escaper": "^3.0.3", + "kleur": "^4.1.4", + "magic-string": "^0.27.0", + "mime": "^3.0.0", + "ora": "^6.1.0", + "path-to-regexp": "^6.2.1", + "preferred-pm": "^3.0.3", + "prompts": "^2.4.2", + "rehype": "^12.0.1", + "semver": "^7.3.8", + "server-destroy": "^1.0.1", + "shiki": "^0.11.1", + "slash": "^4.0.0", + "string-width": "^5.1.2", + "strip-ansi": "^7.0.1", + "supports-esm": "^1.0.0", + "tsconfig-resolver": "^3.0.1", + "typescript": "*", + "unist-util-visit": "^4.1.0", + "vfile": "^5.3.2", + "vite": "^4.2.1", + "vitefu": "^0.2.4", + "yargs-parser": "^21.0.1", + "zod": "^3.17.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "dev": true, + "requires": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + }, + "cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true + }, + "cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "requires": { + "restore-cursor": "^4.0.0" + } + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true + }, + "is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true + }, + "is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true + }, + "log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "requires": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "dependencies": { + "chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "dev": true + } + } + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "ora": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.0.tgz", + "integrity": "sha512-1/D8uRFY0ay2kgBpmAwmSA404w4OoPVhHMqRqtjvrcK/dnzcEZxMJ+V4DUbyICu8IIVRclHcOf5wlD1tMY4GUQ==", + "dev": true, + "requires": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true + }, + "restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true + }, + "widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "requires": { + "string-width": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + } + } + }, + "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==", + "dev": true + } + } + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "requires": { + "lodash": "^4.17.14" + } + }, + "async-lock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", + "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==" + }, + "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==", + "dev": true, + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atlassian-openapi": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/atlassian-openapi/-/atlassian-openapi-1.0.15.tgz", + "integrity": "sha512-HzgdBHJ/9jZWZfass5DRJNG4vLxoFl6Zcl3B+8Cp2VSpEH7t0laBGnGtcthvj2h73hq8dzjKtVlG30agBZ4OPw==", + "dev": true, + "requires": { + "jsonpointer": "^5.0.0", + "urijs": "^1.18.10" + }, + "dependencies": { + "jsonpointer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz", + "integrity": "sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==", + "dev": true + } + } + }, + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, + "bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=" + }, + "basic-auth-parser": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz", + "integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=", + "dev": true + }, + "basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==" + }, + "bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "optional": true + }, + "body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "requires": { + "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.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + } + } + }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "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": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "requires": { + "streamsearch": "^1.1.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "optional": true, + "requires": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "lru-cache": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", + "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==", + "optional": true + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + } + } + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "call-bind": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", + "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "set-function-length": "^1.2.0" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "caniuse-lite": { + "version": "1.0.30001589", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", + "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", + "dev": true + }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "requires": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + } + }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "optional": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true + }, + "chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true + }, + "character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true + }, + "character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "chromium-bidi": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", + "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", + "dev": true, + "requires": { + "mitt": "3.0.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", + "requires": { + "json-parse-helpfulerror": "^1.0.3" + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "devOptional": true + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" + }, + "cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "requires": { + "colors": "1.0.3" + } + }, + "cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "requires": { + "@colors/colors": "1.5.0", + "string-width": "^4.2.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" + }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true + }, + "commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==" + }, + "comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true + }, + "common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "requires": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "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==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "requires": { + "mime-db": ">= 1.40.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + } + } + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "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==", + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "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==" + } + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dev": true, + "requires": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + } + } + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, + "crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "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==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-env": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", + "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "requires": { + "cross-spawn": "^6.0.5", + "is-windows": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + } + } + }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "csv-parse": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", + "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==" + }, + "data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dev": true, + "requires": { + "character-entities": "^2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal-in-any-order": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.6.tgz", + "integrity": "sha512-RfnWHQzph10YrUjvWwhd15Dne8ciSJcZ3U6OD7owPwiVwsdE5IFSoZGg8rlwJD11ES+9H5y8j3fCofviRHOqLQ==", + "requires": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "deepmerge-ts": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-4.3.0.tgz", + "integrity": "sha512-if3ZYdkD2dClhnXR5reKtG98cwyaRT1NeugQoAPTTfsOpV9kqyeiBF9Qa5RHjemb3KzD5ulqygv6ED3t5j9eJw==", + "dev": true + }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "define-data-property": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", + "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "dependencies": { + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "devOptional": true + }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "devalue": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.0.tgz", + "integrity": "sha512-n94yQo4LI3w7erwf84mhRUkUJfhLoCZiLyoOZ/QFsDbcWNZePrLwbQpvZBUG2TNxwV3VjCKPxkiiQA6pe3TrTA==", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1107588", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz", + "integrity": "sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==", + "dev": true + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "requires": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dset": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", + "integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==", + "dev": true + }, + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.4.256", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz", + "integrity": "sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw==", + "dev": true + }, + "emmet": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.2.tgz", + "integrity": "sha512-YgmsMkhUgzhJMgH5noGudfxqrQn1bapvF0y7C1e7A0jWFImsRrrvVslzyZz0919NED/cjFOpVWx7c973V+2S/w==", + "dev": true, + "requires": { + "@emmetio/abbreviation": "^2.3.1", + "@emmetio/css-abbreviation": "^2.1.6" + } + }, + "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==" + }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.x" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true, + "optional": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "optional": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true + }, + "env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==" + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", + "dev": true + }, + "esbuild": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.16.tgz", + "integrity": "sha512-aeSuUKr9aFVY9Dc8ETVELGgkj4urg5isYx8pLf4wlGgB0vTFjxJQdHnNH6Shmx4vYYrOTLCHtRI5i1XZ9l2Zcg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.16", + "@esbuild/android-arm64": "0.17.16", + "@esbuild/android-x64": "0.17.16", + "@esbuild/darwin-arm64": "0.17.16", + "@esbuild/darwin-x64": "0.17.16", + "@esbuild/freebsd-arm64": "0.17.16", + "@esbuild/freebsd-x64": "0.17.16", + "@esbuild/linux-arm": "0.17.16", + "@esbuild/linux-arm64": "0.17.16", + "@esbuild/linux-ia32": "0.17.16", + "@esbuild/linux-loong64": "0.17.16", + "@esbuild/linux-mips64el": "0.17.16", + "@esbuild/linux-ppc64": "0.17.16", + "@esbuild/linux-riscv64": "0.17.16", + "@esbuild/linux-s390x": "0.17.16", + "@esbuild/linux-x64": "0.17.16", + "@esbuild/netbsd-x64": "0.17.16", + "@esbuild/openbsd-x64": "0.17.16", + "@esbuild/sunos-x64": "0.17.16", + "@esbuild/win32-arm64": "0.17.16", + "@esbuild/win32-ia32": "0.17.16", + "@esbuild/win32-x64": "0.17.16" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "requires": {} + }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, + "eslint-plugin-jsdoc": { + "version": "48.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.1.0.tgz", + "integrity": "sha512-g9S8ukmTd1DVcV/xeBYPPXOZ6rc8WJ4yi0+MVxJ1jBOrz5kmxV9gJJQ64ltCqIWFnBChLIhLVx3tbTSarqVyFA==", + "dev": true, + "requires": { + "@es-joy/jsdoccomment": "~0.42.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.6.0", + "spdx-expression-parse": "^4.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + } + } + }, + "eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "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==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==" + }, + "execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "dependencies": { + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + } + } + }, + "exegesis": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.2.tgz", + "integrity": "sha512-D9ZFTFQ8O5ZRBLZ0HAHqo0Gc3+ts330WimHf0cF7OQZLQ3YqRVfjig5qGvEjheS68m+fMjJSR/wN/Qousg17Dw==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^10.3.10", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^3.1.1", + "promise-breaker": "^6.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "requires": { + "exegesis": "^4.1.0" + } + }, + "express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, + "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==" + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "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==" + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "dev": true + }, + "fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "requires": { + "punycode": "^1.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "find-yarn-workspace-root2": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", + "integrity": "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==", + "dev": true, + "requires": { + "micromatch": "^4.0.2", + "pkg-dir": "^4.2.0" + } + }, + "firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "dev": true, + "requires": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "firebase-admin": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.5.0.tgz", + "integrity": "sha512-bBdlYtNvXx8yZGdCd00NrfZl1o1A0aXOw5h8q5PwC8RXikOLNXq8vYtSKW44dj8zIaafVP6jFdcUXZem/LMsHA==", + "dev": true, + "requires": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.3.0", + "@firebase/database-types": "^0.10.0", + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true + } + } + }, + "firebase-functions": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.3.1.tgz", + "integrity": "sha512-sbitfzHcuWsLD03/EgeIRIfkVGeyGjNo3IEA2z+mDIkK1++LhKLCWwVQXrMqeeATOG04CAp30guAagsNElVlng==", + "dev": true, + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7", + "protobufjs": "^7.2.2" + }, + "dependencies": { + "@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true + }, + "protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "requires": { + "@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" + } + } + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", + "dev": true, + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fromentries": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.1.tgz", + "integrity": "sha512-Xu2Qh8yqYuDhQGOhD5iJGninErSfI9A3FrriD3tjUgV5VbJFeH8vfgZ9HnC6jWN80QDVNQK5vmxRAmEAp7Mevw==", + "dev": true + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "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": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true, + "optional": true + }, + "fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==" + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "gaxios": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", + "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "dependencies": { + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "requires": { + "debug": "^4.3.4" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, + "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==", + "requires": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "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==" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==" + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=" + }, + "glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", + "requires": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "requires": { + "ini": "2.0.0" + }, + "dependencies": { + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==" + } + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "google-auth-library": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.7.0.tgz", + "integrity": "sha512-I/AvzBiUXDzLOy4iIZ2W+Zq33W4lcukQv1nl7C8WUA6SQwyQwUwu3waNmWNAvzds//FG8SZ+DnKnW/2k6mQS8A==", + "requires": { + "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" + }, + "dependencies": { + "gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, + "google-discovery-to-swagger": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/google-discovery-to-swagger/-/google-discovery-to-swagger-2.1.0.tgz", + "integrity": "sha512-MI1gfmWPkuXCp6yH+9rfd8ZG8R1R5OIyY4WlKDTqr2+ere1gt2Ne4DSEu8HM7NkwKpuVCE5TrTRAPfm3ownMUQ==", + "dev": true, + "requires": { + "json-schema-compatibility": "^1.1.0", + "jsonpath": "^1.0.2", + "lodash": "^4.17.15", + "mime-db": "^1.21.0", + "mime-lookup": "^0.0.2", + "traverse": "~0.6.6", + "urijs": "^1.17.0" + }, + "dependencies": { + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + } + } + }, + "google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "dev": true, + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "dependencies": { + "@grpc/grpc-js": { + "version": "1.8.21", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.21.tgz", + "integrity": "sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg==", + "dev": true, + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "dev": true, + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + } + }, + "@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "dev": true, + "optional": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "dev": true, + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true, + "optional": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "optional": true + }, + "gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "dev": true, + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "dev": true, + "optional": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "dev": true, + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "optional": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "optional": true, + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "optional": true + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "dev": true, + "optional": true, + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true, + "optional": true + } + } + }, + "protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "googleapis": { + "version": "105.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-105.0.0.tgz", + "integrity": "sha512-wH/jU/6QpqwsjTKj4vfKZz97ne7xT7BBbKwzQEwnbsG8iH9Seyw19P+AuLJcxNNrmgblwLqfr3LORg4Okat1BQ==", + "dev": true, + "requires": { + "google-auth-library": "^8.0.2", + "googleapis-common": "^6.0.0" + }, + "dependencies": { + "gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "dev": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "dev": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.5.1.tgz", + "integrity": "sha512-7jNMDRhenfw2HLfL9m0ZP/Jw5hzXygfSprzBdypG3rZ+q2gIUbVC/osrFB7y/Z5dkrUr1mnLoDNlerF+p6VXZA==", + "dev": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, + "googleapis-common": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.3.tgz", + "integrity": "sha512-Xyb4FsQ6PQDu4tAE/M/ev4yzZhFe2Gc7+rKmuCX2ZGk1ajBKbafsGlVYpmzGqQOT93BRDe8DiTmQb6YSkbICrA==", + "dev": true, + "requires": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "dependencies": { + "gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "dev": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "dev": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.5.1.tgz", + "integrity": "sha512-7jNMDRhenfw2HLfL9m0ZP/Jw5hzXygfSprzBdypG3rZ+q2gIUbVC/osrFB7y/Z5dkrUr1mnLoDNlerF+p6VXZA==", + "dev": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dev": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dev": true, + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true + } + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-package-exports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/has-package-exports/-/has-package-exports-1.3.0.tgz", + "integrity": "sha512-e9OeXPQnmPhYoJ63lXC4wWe34TxEGZDZ3OQX9XRqp2VwsfLl3bQBy7VehLnd34g3ef8CmYlBLGqEMKXuz8YazQ==", + "dev": true, + "requires": { + "@ljharb/has-package-exports-patterns": "^0.0.2" + } + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, + "hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } + } + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + } + }, + "hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0" + } + }, + "hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + } + }, + "hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + } + }, + "hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + } + }, + "hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "dev": true + }, + "hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "heap-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.5.0.tgz", + "integrity": "sha512-kUGoI3p7u6B41z/dp33G6OaL7J4DRqRYwVmeIlwLClx7yaaAy7hoDExnuejTKtuDwfcatGmddHDEOjf6EyIxtQ==" + }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "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==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "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==", + "dev": true + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "dev": true + }, + "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==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "import-meta-resolve": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", + "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "devOptional": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "inquirer-autocomplete-prompt": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-2.0.1.tgz", + "integrity": "sha512-jUHrH0btO7j5r8DTQgANf2CBkTZChoVySD8zF/wp5fZCOLIuUbleXhf4ZY5jNBOc1owA3gdfWtfZuppfYBhcUg==", + "requires": { + "ansi-escapes": "^4.3.2", + "figures": "^3.2.0", + "picocolors": "^1.0.0", + "run-async": "^2.4.1", + "rxjs": "^7.5.4" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "requires": { + "type-fest": "^0.21.3" + } + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + } + } + }, + "install-artifact-from-github": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", + "optional": true + }, + "ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" + }, + "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==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + }, + "is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "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==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "requires": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "requires": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + }, + "dependencies": { + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + } + } + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=" + }, + "join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", + "requires": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "optional": true, + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsdoc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "dev": true, + "optional": true, + "requires": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "optional": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "optional": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "optional": true + } + } + }, + "jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "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==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "requires": { + "jju": "^1.1.0" + } + }, + "json-ptr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.0.1.tgz", + "integrity": "sha512-hrZ4tElT8huJUH3OwOK+d7F8PRqw09QnGM3Mm3GmqKWDyCCPCG8lGHxXOwQAj0VOxzLirOds07Kz10B5F8M8EA==" + }, + "json-schema-compatibility": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-schema-compatibility/-/json-schema-compatibility-1.1.0.tgz", + "integrity": "sha1-GomBd4zaDDgYcpjZmdCJ5Rrygt8=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonc-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "dev": true, + "requires": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + }, + "dependencies": { + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=", + "dev": true + }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "dev": true + } + } + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", + "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", + "dev": true, + "requires": { + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^9.0.0", + "debug": "^4.3.4", + "jose": "^4.10.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true + }, + "kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "requires": { + "colornames": "^1.1.1" + } + }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" + }, + "libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "requires": { + "libsodium": "^0.7.0" + } + }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "optional": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, + "load-yaml-file": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", + "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.13.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + } + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "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==", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "requires": { + "lodash._objecttypes": "~2.4.1" + } + }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=" + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, + "longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dev": true, + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dev": true, + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + }, + "magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "optional": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "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==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", + "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==", + "optional": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "optional": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + } + } + }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true + }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "optional": true, + "requires": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "optional": true + } + } + }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "optional": true, + "requires": {} + }, + "markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "dev": true + }, + "marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==" + }, + "marked-terminal": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", + "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", + "requires": { + "ansi-escapes": "^5.0.0", + "cardinal": "^2.1.1", + "chalk": "^5.0.0", + "cli-table3": "^0.6.1", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.2.0" + }, + "dependencies": { + "chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==" + } + } + }, + "mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true + } + } + }, + "mdast-util-from-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", + "integrity": "sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + } + }, + "mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "dev": true, + "requires": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + } + }, + "mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + } + }, + "mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + } + }, + "mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + } + }, + "mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + } + }, + "mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + } + }, + "mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + } + }, + "mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + } + }, + "mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0" + } + }, + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "optional": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromark": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.1.0.tgz", + "integrity": "sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==", + "dev": true, + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "micromark-core-commonmark": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", + "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "dev": true, + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz", + "integrity": "sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==", + "dev": true, + "requires": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-extension-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-footnote": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.0.tgz", + "integrity": "sha512-RWYce7j8+c0n7Djzv5NzGEGitNNYO3uj+h/XYMdS/JinH1Go+/Qkomg/rfxExFzYTiydaV6GLeffGO5qcJbMPA==", + "dev": true, + "requires": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-strikethrough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.5.tgz", + "integrity": "sha512-X0oI5eYYQVARhiNfbETy7BfLSmSilzN1eOuoRnrf9oUNsPRrWOAe9UqSizgw1vNxQBfOwL+n2610S3bYjVNi7w==", + "dev": true, + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-table": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz", + "integrity": "sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==", + "dev": true, + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dev": true, + "requires": { + "micromark-util-types": "^1.0.0" + } + }, + "micromark-extension-gfm-task-list-item": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.4.tgz", + "integrity": "sha512-9XlIUUVnYXHsFF2HZ9jby4h3npfX10S1coXTnV035QGPgrtNYQq3J6IfIvcCIUAJrrqBVi5BqA/LmaOMJqPwMQ==", + "dev": true, + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-destination": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", + "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-label": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", + "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-space": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", + "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-title": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", + "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "dev": true, + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-whitespace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", + "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "dev": true, + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", + "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "dev": true, + "requires": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-chunked": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", + "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "dev": true, + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-classify-character": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", + "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", + "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "dev": true, + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", + "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "dev": true, + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-decode-string": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", + "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "dev": true, + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-encode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", + "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "dev": true + }, + "micromark-util-html-tag-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", + "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==", + "dev": true + }, + "micromark-util-normalize-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", + "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "dev": true, + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", + "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "dev": true, + "requires": { + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz", + "integrity": "sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==", + "dev": true, + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", + "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "dev": true, + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-util-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", + "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==", + "dev": true + }, + "micromark-util-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", + "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-lookup": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/mime-lookup/-/mime-lookup-0.0.2.tgz", + "integrity": "sha1-o1JdJixC5MraWFmR+FADil1dJB0=", + "dev": true + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "dependencies": { + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + } + } + }, + "minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "optional": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "optional": true + }, + "nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" + }, + "next": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz", + "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==", + "dev": true, + "requires": { + "@next/env": "14.1.0", + "@next/swc-darwin-arm64": "14.1.0", + "@next/swc-darwin-x64": "14.1.0", + "@next/swc-linux-arm64-gnu": "14.1.0", + "@next/swc-linux-arm64-musl": "14.1.0", + "@next/swc-linux-x64-gnu": "14.1.0", + "@next/swc-linux-x64-musl": "14.1.0", + "@next/swc-win32-arm64-msvc": "14.1.0", + "@next/swc-win32-ia32-msvc": "14.1.0", + "@next/swc-win32-x64-msvc": "14.1.0", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + } + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "nlcst-to-string": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz", + "integrity": "sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw==", + "dev": true, + "requires": { + "@types/nlcst": "^1.0.0" + } + }, + "nock": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz", + "integrity": "sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "requires": { + "lodash": "^4.17.21" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "dev": true, + "requires": { + "http2-client": "^1.2.5" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true + }, + "node-gyp": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", + "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", + "optional": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-mocks-http": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.11.0.tgz", + "integrity": "sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw==", + "dev": true, + "requires": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + } + } + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha1-271K8SE04uY1wkXvk//Pb2BnOl0=", + "dev": true, + "requires": { + "es6-promise": "^3.2.1" + } + }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, + "nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "optional": true, + "requires": { + "abbrev": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } + } + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "dev": true, + "requires": { + "fast-safe-stringify": "^2.0.7" + } + }, + "oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "dev": true, + "requires": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } + }, + "oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "dev": true, + "requires": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "dependencies": { + "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==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "requires": { + "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" + } + }, + "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==", + "dev": true + } + } + }, + "oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "dev": true + }, + "oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "requires": { + "is-wsl": "^1.1.0" + } + }, + "openapi-merge": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/openapi-merge/-/openapi-merge-1.3.2.tgz", + "integrity": "sha512-qRWBwPMiKIUrAcKW6lstMPKpFEWy32dBbP1UjHH9jlWgw++2BCqOVbsjO5Wa4H1Ll3c4cn+lyi4TinUy8iswzw==", + "dev": true, + "requires": { + "atlassian-openapi": "^1.0.8", + "lodash": "^4.17.15", + "ts-is-present": "^1.1.1" + } + }, + "openapi-types": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.0.tgz", + "integrity": "sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA==", + "dev": true + }, + "openapi-typescript": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-4.5.0.tgz", + "integrity": "sha512-++gWZLTKmbZP608JHMerllAs84HzULWfVjfH7otkWBLrKxUvzHMFqI6R4JSW1LoNDZnS4KKiRTZW66Fxyp6z4Q==", + "dev": true, + "requires": { + "hosted-git-info": "^3.0.8", + "js-yaml": "^4.1.0", + "kleur": "^4.1.4", + "meow": "^9.0.0", + "mime": "^3.0.0", + "node-fetch": "^2.6.6", + "prettier": "^2.5.1", + "slash": "^3.0.0", + "tiny-glob": "^0.2.9" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "hosted-git-info": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true + }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + } + } + }, + "openapi3-ts": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", + "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", + "requires": { + "yaml": "^2.2.1" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==" + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-throttle": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.1.0.tgz", + "integrity": "sha512-+N+s2g01w1Zch4D0K3OpnPDqLOKmLcQ4BvIFq3JC0K29R28vUOjWpO+OJZBNt8X9i3pFCksZJZ0YXkUGjaFE6g==" + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-latin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-5.0.1.tgz", + "integrity": "sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==", + "dev": true, + "requires": { + "nlcst-to-string": "^3.0.0", + "unist-util-modify-children": "^3.0.0", + "unist-util-visit-children": "^2.0.0" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + } + } + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true + }, + "pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "requires": {} + }, + "pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + } + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + } + } + }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, + "postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, + "preferred-pm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", + "integrity": "sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==", + "dev": true, + "requires": { + "find-up": "^5.0.0", + "find-yarn-workspace-root2": "1.2.16", + "path-exists": "^4.0.0", + "which-pm": "2.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "prettier-plugin-astro": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.7.2.tgz", + "integrity": "sha512-mmifnkG160BtC727gqoimoxnZT/dwr8ASxpoGGl6EHevhfblSOeu+pwH1LAm5Qu1MynizktztFujHHaijLCkww==", + "dev": true, + "requires": { + "@astrojs/compiler": "^0.31.3", + "prettier": "^2.7.1", + "sass-formatter": "^0.7.5", + "synckit": "^0.8.4" + }, + "dependencies": { + "@astrojs/compiler": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-0.31.4.tgz", + "integrity": "sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + } + } + }, + "prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "promise-breaker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz", + "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "dependencies": { + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "dependencies": { + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + } + } + }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, + "property-information": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", + "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" + }, + "proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "dev": true, + "optional": true, + "requires": { + "protobufjs": "^7.0.0" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "dev": true, + "optional": true + }, + "protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "dev": true, + "optional": true, + "requires": { + "@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" + } + } + } + }, + "protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "dev": true, + "requires": { + "@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/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, + "proxy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.2.tgz", + "integrity": "sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==", + "dev": true, + "requires": { + "args": "5.0.1", + "basic-auth-parser": "0.0.2", + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "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==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "optional": true + }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "requires": { + "escape-goat": "^2.0.0" + } + }, + "puppeteer": { + "version": "19.11.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.11.1.tgz", + "integrity": "sha512-39olGaX2djYUdhaQQHDZ0T0GwEp+5f9UB9HmEP0qHfdQHIq0xGQZuAZ5TLnJIc/88SrPLpEflPC+xUqOTv3c5g==", + "dev": true, + "requires": { + "@puppeteer/browsers": "0.5.0", + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "19.11.1" + } + }, + "puppeteer-core": { + "version": "19.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.11.1.tgz", + "integrity": "sha512-qcuC2Uf0Fwdj9wNtaTZ2OvYRraXpAK+puwwVW8ofOhOgLPZyz1c68tsorfIZyCUOpyBisjr+xByu7BMbEYMepA==", + "dev": true, + "requires": { + "@puppeteer/browsers": "0.5.0", + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1107588", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "requires": {} + } + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "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==" + }, + "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==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "re2": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.18.0.tgz", + "integrity": "sha512-MoCYZlJ9YUgksND9asyNF2/x532daXU/ARp1UeJbQ5flMY6ryKNEhrWt85aw3YluzOJlC3vXpGgK2a1jb0b4GA==", + "optional": true, + "requires": { + "install-artifact-from-github": "^1.3.1", + "nan": "^2.17.0", + "node-gyp": "^9.3.0" + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "requires": { + "esprima": "~4.0.0" + } + }, + "reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "requires": { + "rc": "^1.2.8" + } + }, + "rehype": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz", + "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "rehype-parse": "^8.0.0", + "rehype-stringify": "^9.0.0", + "unified": "^10.0.0" + } + }, + "rehype-parse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", + "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" + } + }, + "rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + } + }, + "rehype-stringify": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", + "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" + } + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + } + }, + "remark-parse": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", + "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==", + "dev": true, + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + } + }, + "remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dev": true, + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + } + }, + "remark-smartypants": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-2.0.0.tgz", + "integrity": "sha512-Rc0VDmr/yhnMQIz8n2ACYXlfw/P/XZev884QU1I5u+5DgJls32o97Vc1RbK3pfumLsJomS2yy8eT4Fxj/2MDVA==", + "dev": true, + "requires": { + "retext": "^8.1.0", + "retext-smartypants": "^5.1.0", + "unist-util-visit": "^4.1.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "retext": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-8.1.0.tgz", + "integrity": "sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q==", + "dev": true, + "requires": { + "@types/nlcst": "^1.0.0", + "retext-latin": "^3.0.0", + "retext-stringify": "^3.0.0", + "unified": "^10.0.0" + } + }, + "retext-latin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-3.1.0.tgz", + "integrity": "sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==", + "dev": true, + "requires": { + "@types/nlcst": "^1.0.0", + "parse-latin": "^5.0.0", + "unherit": "^3.0.0", + "unified": "^10.0.0" + } + }, + "retext-smartypants": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-5.2.0.tgz", + "integrity": "sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw==", + "dev": true, + "requires": { + "@types/nlcst": "^1.0.0", + "nlcst-to-string": "^3.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "retext-stringify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-3.1.0.tgz", + "integrity": "sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w==", + "dev": true, + "requires": { + "@types/nlcst": "^1.0.0", + "nlcst-to-string": "^3.0.0", + "unified": "^10.0.0" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + }, + "retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + } + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", + "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", + "requires": { + "glob": "^10.3.7" + } + }, + "rollup": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", + "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "router": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", + "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", + "requires": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "dependencies": { + "array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==" + } + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "s.color": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", + "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", + "dev": true + }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "requires": { + "mri": "^1.1.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sass-formatter": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.6.tgz", + "integrity": "sha512-hXdxU6PCkiV3XAiSnX+XLqz2ohHoEnVUlrd8LEVMAI80uB1+OTScIkH9n6qQwImZpTye1r1WG1rbGUteHNhoHg==", + "dev": true, + "requires": { + "suf-log": "^2.5.3" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "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" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "devOptional": true + }, + "set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "requires": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shiki": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.11.1.tgz", + "integrity": "sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==", + "dev": true, + "requires": { + "jsonc-parser": "^3.0.0", + "vscode-oniguruma": "^1.6.1", + "vscode-textmate": "^6.0.0" + } + }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, + "sinon": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.3.tgz", + "integrity": "sha512-m+DyAWvqVHZtjnjX/nuShasykFeiZ+nPuEfD4G3gpvKGkXRhkF/6NSt2qN2FjZhfrcHXFzUzI+NLnk+42fnLEw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "sinon-chai": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.6.0.tgz", + "integrity": "sha512-bk2h+0xyKnmvazAnc7HE5esttqmCerSMcBtuB2PS2T4tG6x8woXAxZeJaOJWD+8reXHngnXn0RtIbfEW9OTHFg==", + "dev": true, + "requires": {} + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "dependencies": { + "ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + } + } + }, + "socks-proxy-agent": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "requires": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "requires": { + "lodash": "^4.17.21" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sql-formatter": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.0.tgz", + "integrity": "sha512-1aDYVEX+dwOSCkRYns4HEGupRZoaivcsNpU4IzR+MVC+cWFYK9/dce7pr4aId4+ED2iK9PNs3j1Vdf8C+SIvDg==", + "requires": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + } + } + }, + "ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "optional": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "dev": true, + "requires": { + "escodegen": "^1.8.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "requires": { + "bl": "^5.0.0" + }, + "dependencies": { + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + } + } + }, + "stream-chain": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.4.tgz", + "integrity": "sha512-9lsl3YM53V5N/I1C2uJtc3Kavyi3kNYN83VkKb/bMWRk7D9imiFyUPYa0PoZbLohSVOX1mYE9YsmwObZUsth6Q==" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-json": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.3.tgz", + "integrity": "sha512-Y6dXn9KKWSwxOqnvHGcdZy1PK+J+7alBwHCeU3W9oRqm4ilLRA0XSPmd1tWwhg7tv9EIxJTMWh7KF15tYelKJg==", + "requires": { + "stream-chain": "^2.2.4" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true + }, + "streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "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==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dev": true, + "requires": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" + }, + "styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dev": true, + "requires": { + "client-only": "0.0.1" + } + }, + "suf-log": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", + "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", + "dev": true, + "requires": { + "s.color": "0.0.15" + } + }, + "superagent": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.3.tgz", + "integrity": "sha512-WA6et4nAvgBCS73lJvv1D0ssI5uk5Gh+TGN/kNe+B608EtcVs/yzfl+OLXTzDs7tOBDIpvgh/WUs1K2OK1zTeQ==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "^2.5.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "requires": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "re2": "^1.17.7", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "supertest": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.3.tgz", + "integrity": "sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^7.1.3" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-esm/-/supports-esm-1.0.0.tgz", + "integrity": "sha512-96Am8CDqUaC0I2+C/swJ0yEvM8ZnGn4unoers/LSdE4umhX7mELzqyLzx3HnZAluq5PXIsGMKqa7NkqaeHMPcg==", + "dev": true, + "requires": { + "has-package-exports": "^1.1.0" + } + }, + "supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "dependencies": { + "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==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "requires": { + "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" + } + }, + "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==", + "dev": true + } + } + }, + "synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "requires": { + "debug": "4.3.1", + "is2": "^2.0.6" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "dev": true, + "optional": true, + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "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==", + "dev": true, + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "optional": true + } + } + }, + "term-size": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", + "dev": true + }, + "terser": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "requires": { + "b4a": "^1.6.4" + } + }, + "text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==", + "dev": true + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "requires": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "requires": { + "lodash": "^4.17.10" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true + }, + "trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "dev": true + }, + "ts-is-present": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ts-is-present/-/ts-is-present-1.1.5.tgz", + "integrity": "sha512-7cTV1I0C58HusRxMXTgbAIFu54tB+ZqGX/nf4YuePFiz40NHQbQVBgZSws1No/DJYnGf5Mx26PcyLPol01t5DQ==", + "dev": true + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tsconfig-resolver": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tsconfig-resolver/-/tsconfig-resolver-3.0.1.tgz", + "integrity": "sha512-ZHqlstlQF449v8glscGRXzL6l2dZvASPCdXJRWG4gHEZlUVx2Jtmr+a2zeVG4LCsKhDXKRj5R3h0C/98UcVAQg==", + "dev": true, + "requires": { + "@types/json5": "^0.0.30", + "@types/resolve": "^1.17.0", + "json5": "^2.1.3", + "resolve": "^1.17.0", + "strip-bom": "^4.0.0", + "type-fest": "^0.13.1" + }, + "dependencies": { + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + } + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + }, + "typescript-json-schema": { + "version": "0.50.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.50.1.tgz", + "integrity": "sha512-GCof/SDoiTDl0qzPonNEV4CHyCsZEIIf+mZtlrjoD8vURCcEzEfa2deRuxYid8Znp/e27eDR7Cjg8jgGrimBCA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.7", + "@types/node": "^14.14.33", + "glob": "^7.1.6", + "json-stable-stringify": "^1.0.1", + "ts-node": "^9.1.1", + "typescript": "~4.2.3", + "yargs": "^16.2.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true + } + } + }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "optional": true + }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true, + "optional": true + }, + "undici": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.21.2.tgz", + "integrity": "sha512-f6pTQ9RF4DQtwoWSaC42P/NKlUjvezVvd9r155ohqkwFNRyBKM3f3pcty3ouusefNRyM25XhIQEbeQ46sZDJfQ==", + "dev": true, + "requires": { + "busboy": "^1.6.0" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "unherit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-3.0.1.tgz", + "integrity": "sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==", + "dev": true + }, + "unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "dependencies": { + "is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true + } + } + }, + "unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "optional": true, + "requires": { + "unique-slug": "^3.0.0" + } + }, + "unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "dev": true + }, + "unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-modify-children": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-3.1.1.tgz", + "integrity": "sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "array-iterate": "^2.0.0" + } + }, + "unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + } + }, + "unist-util-visit-children": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-2.0.2.tgz", + "integrity": "sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0" + } + }, + "unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + } + }, + "universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "requires": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "update-browserslist-db": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", + "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "requires": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "registry-auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", + "integrity": "sha512-UfxVOj8seK1yaIOiieV4FIP01vfBDLsY0H9sQzi9EbbUdJiuuBjJgLa1DpImXMNPnVkBD4eVxTEXcrZA6kfpJA==", + "requires": { + "@pnpm/npm-conf": "^1.0.4" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "dev": true + }, + "url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=" + }, + "url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dev": true, + "requires": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + } + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + } + }, + "vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + } + }, + "vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + } + }, + "vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==", + "dev": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.18.0" + } + }, + "vitefu": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", + "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", + "dev": true, + "requires": {} + }, + "vscode-css-languageservice": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.2.4.tgz", + "integrity": "sha512-9UG0s3Ss8rbaaPZL1AkGzdjrGY8F+P+Ne9snsrvD9gxltDGhsn8C2dQpqQewHrMW37OvlqJoI8sUU2AWDb+qNw==", + "dev": true, + "requires": { + "@vscode/l10n": "^0.0.11", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + } + }, + "vscode-html-languageservice": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.0.4.tgz", + "integrity": "sha512-tvrySfpglu4B2rQgWGVO/IL+skvU7kBkQotRlxA7ocSyRXOZUd6GA13XHkxo8LPe07KWjeoBlN1aVGqdfTK4xA==", + "dev": true, + "requires": { + "@vscode/l10n": "^0.0.11", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.2", + "vscode-uri": "^3.0.7" + } + }, + "vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "dev": true + }, + "vscode-languageserver": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", + "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", + "dev": true, + "requires": { + "vscode-languageserver-protocol": "3.17.3" + } + }, + "vscode-languageserver-protocol": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "dev": true, + "requires": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz", + "integrity": "sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==", + "dev": true + }, + "vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==", + "dev": true + }, + "vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "vscode-textmate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-6.0.0.tgz", + "integrity": "sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==", + "dev": true + }, + "vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==", + "dev": true + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "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==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "which-pm": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.0.0.tgz", + "integrity": "sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==", + "dev": true, + "requires": { + "load-yaml-file": "^0.2.0", + "path-exists": "^4.0.0" + } + }, + "which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "dev": true + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" + } + }, + "winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "requires": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + } + }, + "winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "requires": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "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==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "optional": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + } + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "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==" + }, + "zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "requires": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "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==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "dev": true + }, + "zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true + } + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 4224b76f366..00000000000 --- a/package-lock.json +++ /dev/null @@ -1,11249 +0,0 @@ -{ - "name": "firebase-tools", - "version": "10.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@apidevtools/json-schema-ref-parser": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", - "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", - "requires": { - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "js-yaml": "^3.13.1" - } - }, - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", - "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.6", - "@babel/helper-module-transforms": "^7.11.0", - "@babel/helpers": "^7.10.4", - "@babel/parser": "^7.11.5", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.11.5", - "@babel/types": "^7.11.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", - "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", - "dev": true, - "requires": { - "@babel/types": "^7.11.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", - "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", - "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", - "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.10.4", - "@babel/helper-replace-supers": "^7.10.4", - "@babel/helper-simple-access": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/template": "^7.10.4", - "@babel/types": "^7.11.0", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-replace-supers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", - "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", - "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "dev": true, - "requires": { - "@babel/types": "^7.11.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/helpers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", - "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", - "dev": true, - "requires": { - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", - "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", - "dev": true - }, - "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } - } - }, - "@babel/traverse": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", - "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.11.5", - "@babel/types": "^7.11.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", - "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.19", - "to-fast-properties": "^2.0.0" - } - }, - "@eslint/eslintrc": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", - "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "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==", - "dev": true - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - } - } - }, - "@exodus/schemasafe": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.0.0-rc.2.tgz", - "integrity": "sha512-W98NvvOe/Med3o66xTO03pd7a2omZebH79PV64gSE+ceDdU8uxQhFTa7ISiD1kseyqyOrMyW5/MNdsGEU02i3Q==", - "dev": true - }, - "@firebase/analytics": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.6.0.tgz", - "integrity": "sha512-6qYEOPUVYrMhqvJ46Z5Uf1S4uULd6d7vGpMP5Qz+u8kIWuOQGcPdJKQap+Hla6Rq164or9gC2HRXuYXKlgWfpw==", - "dev": true, - "requires": { - "@firebase/analytics-types": "0.4.0", - "@firebase/component": "0.1.19", - "@firebase/installations": "0.4.17", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/analytics-types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.4.0.tgz", - "integrity": "sha512-Jj2xW+8+8XPfWGkv9HPv/uR+Qrmq37NPYT352wf7MvE9LrstpLVmFg3LqG6MCRr5miLAom5sen2gZ+iOhVDeRA==", - "dev": true - }, - "@firebase/app": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.11.tgz", - "integrity": "sha512-FH++PaoyTzfTAVuJ0gITNYEIcjT5G+D0671La27MU8Vvr6MTko+5YUZ4xS9QItyotSeRF4rMJ1KR7G8LSyySiA==", - "dev": true, - "requires": { - "@firebase/app-types": "0.6.1", - "@firebase/component": "0.1.19", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", - "dom-storage": "2.1.0", - "tslib": "^1.11.1", - "xmlhttprequest": "1.8.0" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/app-types": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", - "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==", - "dev": true - }, - "@firebase/auth": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.15.0.tgz", - "integrity": "sha512-IFuzhxS+HtOQl7+SZ/Mhaghy/zTU7CENsJFWbC16tv2wfLZbayKF5jYGdAU3VFLehgC8KjlcIWd10akc3XivfQ==", - "dev": true, - "requires": { - "@firebase/auth-types": "0.10.1" - } - }, - "@firebase/auth-interop-types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", - "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==", - "dev": true - }, - "@firebase/auth-types": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", - "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", - "dev": true - }, - "@firebase/component": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.21.tgz", - "integrity": "sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg==", - "dev": true, - "requires": { - "@firebase/util": "0.3.4", - "tslib": "^1.11.1" - } - }, - "@firebase/database": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.1.tgz", - "integrity": "sha512-/1HhR4ejpqUaM9Cn3KSeNdQvdlehWIhdfTVWFxS73ZlLYf7ayk9jITwH10H3ZOIm5yNzxF67p/U7Z/0IPhgWaQ==", - "dev": true, - "requires": { - "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.21", - "@firebase/database-types": "0.6.1", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.4", - "faye-websocket": "0.11.3", - "tslib": "^1.11.1" - } - }, - "@firebase/database-types": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.6.1.tgz", - "integrity": "sha512-JtL3FUbWG+bM59iYuphfx9WOu2Mzf0OZNaqWiQ7lJR8wBe7bS9rIm9jlBFtksB7xcya1lZSQPA/GAy2jIlMIkA==", - "dev": true, - "requires": { - "@firebase/app-types": "0.6.1" - } - }, - "@firebase/firestore": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-1.18.0.tgz", - "integrity": "sha512-maMq4ltkrwjDRusR2nt0qS4wldHQMp+0IDSfXIjC+SNmjnWY/t/+Skn9U3Po+dB38xpz3i7nsKbs+8utpDnPSw==", - "dev": true, - "requires": { - "@firebase/component": "0.1.19", - "@firebase/firestore-types": "1.14.0", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", - "@firebase/webchannel-wrapper": "0.4.0", - "@grpc/grpc-js": "^1.0.0", - "@grpc/proto-loader": "^0.5.0", - "node-fetch": "2.6.1", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/firestore-types": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-1.14.0.tgz", - "integrity": "sha512-WF8IBwHzZDhwyOgQnmB0pheVrLNP78A8PGxk1nxb/Nrgh1amo4/zYvFMGgSsTeaQK37xMYS/g7eS948te/dJxw==", - "dev": true - }, - "@firebase/functions": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.5.1.tgz", - "integrity": "sha512-yyjPZXXvzFPjkGRSqFVS5Hc2Y7Y48GyyMH+M3i7hLGe69r/59w6wzgXKqTiSYmyE1pxfjxU4a1YqBDHNkQkrYQ==", - "dev": true, - "requires": { - "@firebase/component": "0.1.19", - "@firebase/functions-types": "0.3.17", - "@firebase/messaging-types": "0.5.0", - "node-fetch": "2.6.1", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/functions-types": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.3.17.tgz", - "integrity": "sha512-DGR4i3VI55KnYk4IxrIw7+VG7Q3gA65azHnZxo98Il8IvYLr2UTBlSh72dTLlDf25NW51HqvJgYJDKvSaAeyHQ==", - "dev": true - }, - "@firebase/installations": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.4.17.tgz", - "integrity": "sha512-AE/TyzIpwkC4UayRJD419xTqZkKzxwk0FLht3Dci8WI2OEKHSwoZG9xv4hOBZebe+fDzoV2EzfatQY8c/6Avig==", - "dev": true, - "requires": { - "@firebase/component": "0.1.19", - "@firebase/installations-types": "0.3.4", - "@firebase/util": "0.3.2", - "idb": "3.0.2", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/installations-types": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.3.4.tgz", - "integrity": "sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q==", - "dev": true - }, - "@firebase/logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", - "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", - "dev": true - }, - "@firebase/messaging": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.7.1.tgz", - "integrity": "sha512-iev/ST9v0xd/8YpGYrZtDcqdD9J6ZWzSuceRn8EKy5vIgQvW/rk2eTQc8axzvDpQ36ZfphMYuhW6XuNrR3Pd2Q==", - "dev": true, - "requires": { - "@firebase/component": "0.1.19", - "@firebase/installations": "0.4.17", - "@firebase/messaging-types": "0.5.0", - "@firebase/util": "0.3.2", - "idb": "3.0.2", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/messaging-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/messaging-types/-/messaging-types-0.5.0.tgz", - "integrity": "sha512-QaaBswrU6umJYb/ZYvjR5JDSslCGOH6D9P136PhabFAHLTR4TWjsaACvbBXuvwrfCXu10DtcjMxqfhdNIB1Xfg==", - "dev": true - }, - "@firebase/performance": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.4.2.tgz", - "integrity": "sha512-irHTCVWJ/sxJo0QHg+yQifBeVu8ZJPihiTqYzBUz/0AGc51YSt49FZwqSfknvCN2+OfHaazz/ARVBn87g7Ex8g==", - "dev": true, - "requires": { - "@firebase/component": "0.1.19", - "@firebase/installations": "0.4.17", - "@firebase/logger": "0.2.6", - "@firebase/performance-types": "0.0.13", - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/performance-types": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.0.13.tgz", - "integrity": "sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA==", - "dev": true - }, - "@firebase/polyfill": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@firebase/polyfill/-/polyfill-0.3.36.tgz", - "integrity": "sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg==", - "dev": true, - "requires": { - "core-js": "3.6.5", - "promise-polyfill": "8.1.3", - "whatwg-fetch": "2.0.4" - } - }, - "@firebase/remote-config": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.1.28.tgz", - "integrity": "sha512-4zSdyxpt94jAnFhO8toNjG8oMKBD+xTuBIcK+Nw8BdQWeJhEamgXlupdBARUk1uf3AvYICngHH32+Si/dMVTbw==", - "dev": true, - "requires": { - "@firebase/component": "0.1.19", - "@firebase/installations": "0.4.17", - "@firebase/logger": "0.2.6", - "@firebase/remote-config-types": "0.1.9", - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/remote-config-types": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz", - "integrity": "sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA==", - "dev": true - }, - "@firebase/storage": { - "version": "0.3.43", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.3.43.tgz", - "integrity": "sha512-Jp54jcuyimLxPhZHFVAhNbQmgTu3Sda7vXjXrNpPEhlvvMSq4yuZBR6RrZxe/OrNVprLHh/6lTCjwjOVSo3bWA==", - "dev": true, - "requires": { - "@firebase/component": "0.1.19", - "@firebase/storage-types": "0.3.13", - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "@firebase/storage-types": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.3.13.tgz", - "integrity": "sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog==", - "dev": true - }, - "@firebase/util": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz", - "integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - }, - "@firebase/webchannel-wrapper": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz", - "integrity": "sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ==", - "dev": true - }, - "@google-cloud/common": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz", - "integrity": "sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==", - "dev": true, - "optional": true, - "requires": { - "@google-cloud/projectify": "^2.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^6.1.1", - "retry-request": "^4.1.1", - "teeny-request": "^7.0.0" - } - }, - "@google-cloud/firestore": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.8.0.tgz", - "integrity": "sha512-cBPo7QQG+aUhS7AIr6fDlA9KIX0/U26rKZyL2K/L68LArDQzgBk1/xOiMoflHRNDQARwCQ0PAZmw8V8CXg7vTg==", - "dev": true, - "optional": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^2.9.2" - }, - "dependencies": { - "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==", - "dev": true, - "optional": true - } - } - }, - "@google-cloud/paginator": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz", - "integrity": "sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==", - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "@google-cloud/precise-date": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.3.tgz", - "integrity": "sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA==" - }, - "@google-cloud/projectify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", - "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==" - }, - "@google-cloud/promisify": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", - "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" - }, - "@google-cloud/pubsub": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-2.7.0.tgz", - "integrity": "sha512-wc/XOo5Ibo3GWmuaLu80EBIhXSdu2vf99HUqBbdsSSkmRNIka2HqoIhLlOFnnncQn0lZnGL7wtKGIDLoH9LiBg==", - "requires": { - "@google-cloud/paginator": "^3.0.0", - "@google-cloud/precise-date": "^2.0.0", - "@google-cloud/projectify": "^2.0.0", - "@google-cloud/promisify": "^2.0.0", - "@opentelemetry/api": "^0.11.0", - "@opentelemetry/tracing": "^0.11.0", - "@types/duplexify": "^3.6.0", - "@types/long": "^4.0.0", - "arrify": "^2.0.0", - "extend": "^3.0.2", - "google-auth-library": "^6.1.2", - "google-gax": "^2.9.2", - "is-stream-ended": "^0.1.4", - "lodash.snakecase": "^4.1.1", - "p-defer": "^3.0.0" - } - }, - "@google-cloud/storage": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.7.0.tgz", - "integrity": "sha512-6nPTylNaYWsVo5yHDdjQfUSh9qP/DFwahhyvOAf9CSDKfeoOys8+PAyHsoKyL29uyYoC6ymws7uJDO48y/SzBA==", - "dev": true, - "optional": true, - "requires": { - "@google-cloud/common": "^3.5.0", - "@google-cloud/paginator": "^3.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.0", - "compressible": "^2.0.12", - "date-and-time": "^0.14.0", - "duplexify": "^4.0.0", - "extend": "^3.0.2", - "gaxios": "^4.0.0", - "gcs-resumable-upload": "^3.1.0", - "get-stream": "^6.0.0", - "hash-stream-validation": "^0.2.2", - "mime": "^2.2.0", - "mime-types": "^2.0.8", - "onetime": "^5.1.0", - "p-limit": "^3.0.1", - "pumpify": "^2.0.0", - "snakeize": "^0.1.0", - "stream-events": "^1.0.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "get-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", - "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", - "dev": true, - "optional": true - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", - "dev": true, - "optional": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "optional": true - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "optional": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "optional": true, - "requires": { - "yocto-queue": "^0.1.0" - } - } - } - }, - "@google/events": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@google/events/-/events-5.1.1.tgz", - "integrity": "sha512-97u6AUfEXo6TxoBAdbziuhSL56+l69WzFahR6eTQE/bSjGPqT1+W4vS7eKaR7r60pGFrZZfqdFZ99uMbns3qgA==", - "dev": true - }, - "@grpc/grpc-js": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.8.tgz", - "integrity": "sha512-64hg5rmEm6F/NvlWERhHmmgxbWU8nD2TMWE+9TvG7/WcOrFT3fzg/Uu631pXRFwmJ4aWO/kp9vVSlr8FUjBDLA==", - "requires": { - "@grpc/proto-loader": "^0.6.0-pre14", - "@types/node": "^12.12.47", - "google-auth-library": "^6.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "@grpc/proto-loader": { - "version": "0.6.0-pre9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.0-pre9.tgz", - "integrity": "sha512-oM+LjpEjNzW5pNJjt4/hq1HYayNeQT+eGrOPABJnYHv7TyNPDNzkQ76rDYZF86X5swJOa4EujEMzQ9iiTdPgww==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.9.0", - "yargs": "^15.3.1" - } - }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "@types/node": { - "version": "12.19.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.9.tgz", - "integrity": "sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==" - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "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==", - "requires": { - "color-name": "~1.1.4" - } - }, - "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==" - }, - "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==" - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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==" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "protobufjs": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", - "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", - "requires": { - "@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/long": "^4.0.1", - "@types/node": "^13.7.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "13.13.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.36.tgz", - "integrity": "sha512-ctzZJ+XsmHQwe3xp07gFUq4JxBaRSYzKHPgblR76//UanGST7vfFNF0+ty5eEbgTqsENopzoDK090xlha9dccQ==" - } - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "@grpc/proto-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.1.tgz", - "integrity": "sha512-3y0FhacYAwWvyXshH18eDkUI40wT/uGio7MAegzY8lO5+wVsc19+1A7T0pPptae4kl7bdITL+0cHpnAPmryBjQ==", - "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - } - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", - "dev": true - }, - "@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" - }, - "@manifoldco/swagger-to-ts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@manifoldco/swagger-to-ts/-/swagger-to-ts-2.1.0.tgz", - "integrity": "sha512-IH0FAHhwWHR3Gs3rnVHNEscZujGn+K6/2Zu5cWfZre3Vz2tx1SvvJKEbSM89MztfDDRjOpb+6pQD/vqdEoTBVg==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "js-yaml": "^3.13.1", - "meow": "^7.0.0", - "prettier": "^2.0.5" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "prettier": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", - "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.4", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", - "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.4", - "fastq": "^1.6.0" - } - }, - "@opentelemetry/api": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.11.0.tgz", - "integrity": "sha512-K+1ADLMxduhsXoZ0GRfi9Pw162FvzBQLDQlHru1lg86rpIU+4XqdJkSGo6y3Kg+GmOWq1HNHOA/ydw/rzHQkRg==", - "requires": { - "@opentelemetry/context-base": "^0.11.0" - } - }, - "@opentelemetry/context-base": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.11.0.tgz", - "integrity": "sha512-ESRk+572bftles7CVlugAj5Azrz61VO0MO0TS2pE9MLVL/zGmWuUBQryART6/nsrFqo+v9HPt37GPNcECTZR1w==" - }, - "@opentelemetry/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-0.11.0.tgz", - "integrity": "sha512-ZEKjBXeDGBqzouz0uJmrbEKNExEsQOhsZ3tJDCLcz5dUNoVw642oIn2LYWdQK2YdIfZbEmltiF65/csGsaBtFA==", - "requires": { - "@opentelemetry/api": "^0.11.0", - "@opentelemetry/context-base": "^0.11.0", - "semver": "^7.1.3" - }, - "dependencies": { - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@opentelemetry/resources": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-0.11.0.tgz", - "integrity": "sha512-o7DwV1TcezqBtS5YW2AWBcn01nVpPptIbTr966PLlVBcS//w8LkjeOShiSZxQ0lmV4b2en0FiSouSDoXk/5qIQ==", - "requires": { - "@opentelemetry/api": "^0.11.0", - "@opentelemetry/core": "^0.11.0" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-0.11.0.tgz", - "integrity": "sha512-xsthnI/J+Cx0YVDGgUzvrH0ZTtfNtl866M454NarYwDrc0JvC24sYw+XS5PJyk2KDzAHtb0vlrumUc1OAut/Fw==" - }, - "@opentelemetry/tracing": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/tracing/-/tracing-0.11.0.tgz", - "integrity": "sha512-QweFmxzl32BcyzwdWCNjVXZT1WeENNS/RWETq/ohqu+fAsTcMyGcr6cOq/yDdFmtBy+bm5WVVdeByEjNS+c4/w==", - "requires": { - "@opentelemetry/api": "^0.11.0", - "@opentelemetry/context-base": "^0.11.0", - "@opentelemetry/core": "^0.11.0", - "@opentelemetry/resources": "^0.11.0", - "@opentelemetry/semantic-conventions": "^0.11.0" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" - }, - "@sinonjs/commons": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", - "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@sinonjs/samsam": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", - "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" - }, - "@types/archiver": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.1.0.tgz", - "integrity": "sha512-baFOhanb/hxmcOd1Uey2TfFg43kTSmM6py1Eo7Rjbv/ivcl7PXLhY0QgXGf50Hx/eskGCFqPfhs/7IZLb15C5g==", - "requires": { - "@types/glob": "*" - } - }, - "@types/body-parser": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", - "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", - "dev": true - }, - "@types/chai": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz", - "integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==", - "dev": true - }, - "@types/chai-as-promised": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz", - "integrity": "sha512-FQnh1ohPXJELpKhzjuDkPLR2BZCAqed+a6xV4MI/T3XzHfd2FlarfUGUdZYgqYe8oxkYn0fchHEeHfHqdZ96sg==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, - "@types/cjson": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@types/cjson/-/cjson-0.5.0.tgz", - "integrity": "sha512-fZdrvfhUxvBDQ5+mksCUvUE+nLXwG416gz+iRdYGDEsQQD5mH0PeLzH0ACuRPbobpVvzKjDHo9VYpCKb1EwLIw==", - "dev": true - }, - "@types/cli-color": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@types/cli-color/-/cli-color-0.3.29.tgz", - "integrity": "sha1-yDpx/gLIx+HM7ASN1qJFjR9sluo=", - "dev": true - }, - "@types/cli-table": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.0.tgz", - "integrity": "sha512-QnZUISJJXyhyD6L1e5QwXDV/A5i2W1/gl6D6YMc8u0ncPepbv/B4w3S+izVvtAg60m6h+JP09+Y/0zF2mojlFQ==", - "dev": true - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, - "@types/configstore": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-4.0.0.tgz", - "integrity": "sha512-SvCBBPzOIe/3Tu7jTl2Q8NjITjLmq9m7obzjSyb8PXWWZ31xVK6w4T6v8fOx+lrgQnqk3Yxc00LDolFsSakKCA==", - "dev": true - }, - "@types/connect": { - "version": "3.4.32", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", - "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/cookiejar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", - "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", - "dev": true - }, - "@types/cors": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", - "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==", - "dev": true - }, - "@types/cross-spawn": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.1.tgz", - "integrity": "sha512-MtN1pDYdI6D6QFDzy39Q+6c9rl2o/xN7aWGe6oZuzqq5N6+YuwFsWiEAv3dNzvzN9YzU+itpN8lBzFpphQKLAw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/dotenv": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz", - "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", - "requires": { - "@types/node": "*" - } - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" - }, - "@types/express": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", - "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.8.tgz", - "integrity": "sha512-1SJZ+R3Q/7mLkOD9ewCBDYD2k0WyZQtWYqF/2VvoNN2/uhI49J9CDN4OAm+wGMA0DbArA4ef27xl4+JwMtGggw==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/fs-extra": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.1.0.tgz", - "integrity": "sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/inquirer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.0.3.tgz", - "integrity": "sha512-lBsdZScFMaFYYIE3Y6CWX22B9VeY2NerT1kyU2heTc3u/W6a+Om6Au2q0rMzBrzynN0l4QoABhI0cbNdyz6fDg==", - "dev": true, - "requires": { - "@types/through": "*", - "rxjs": "^6.4.0" - } - }, - "@types/js-yaml": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.2.tgz", - "integrity": "sha512-0CFu/g4mDSNkodVwWijdlr8jH7RoplRWNgovjFLEZeT+QEbbZXjBmCe3HwaWheAlCbHwomTwzZoSedeOycABug==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", - "dev": true - }, - "@types/jsonwebtoken": { - "version": "8.3.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.8.tgz", - "integrity": "sha512-g2ke5+AR/RKYpQxd+HJ2yisLHGuOV0uourOcPtKlcT5Zqv4wFg9vKhFpXEztN4H/6Y6RSUKioz/2PTFPP30CTA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/lodash": { - "version": "4.14.149", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", - "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", - "dev": true - }, - "@types/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" - }, - "@types/marked": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.6.5.tgz", - "integrity": "sha512-6kBKf64aVfx93UJrcyEZ+OBM5nGv4RLsI6sR1Ar34bpgvGVRoyTgpxn4ZmtxOM5aDTAaaznYuYUH8bUX3Nk3YA==", - "dev": true - }, - "@types/mime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" - }, - "@types/minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", - "dev": true - }, - "@types/minipass": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-2.2.0.tgz", - "integrity": "sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/mocha": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.0.tgz", - "integrity": "sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ==", - "dev": true - }, - "@types/multer": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz", - "integrity": "sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/node": { - "version": "10.17.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.50.tgz", - "integrity": "sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA==" - }, - "@types/node-fetch": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", - "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", - "dev": true, - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-bPOsfCZ4tsTlKiBjBhKnM8jpY5nmIll166IPD58D92hR7G7kZDfx5iB9wGF4NfZrdKolebjeAr3GouYkSGoJ/A==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/puppeteer": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.2.tgz", - "integrity": "sha512-yjbHoKjZFOGqA6bIEI2dfBE5UPqU0YGWzP+ipDVP1iGzmlhksVKTBVZfT3Aj3wnvmcJ2PQ9zcncwOwyavmafBw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", - "dev": true - }, - "@types/request": { - "version": "2.48.2", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.2.tgz", - "integrity": "sha512-gP+PSFXAXMrd5PcD7SqHeUjdGshAI8vKQ3+AvpQr3ht9iQea+59LOKvKITcQI+Lg+1EIkDP6AFSBUJPWG8GDyA==", - "dev": true, - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.0.tgz", - "integrity": "sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } - } - }, - "@types/rimraf": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-2.0.3.tgz", - "integrity": "sha512-dZfyfL/u9l/oi984hEXdmAjX3JHry7TLWw43u1HQ8HhPv6KtfxnrZ3T/bleJ0GEvnk9t5sM7eePkgMqz3yBcGg==", - "dev": true, - "requires": { - "@types/glob": "*", - "@types/node": "*" - } - }, - "@types/semver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", - "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==", - "dev": true - }, - "@types/serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", - "dev": true, - "requires": { - "@types/express-serve-static-core": "*", - "@types/mime": "*" - } - }, - "@types/sinon": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.10.tgz", - "integrity": "sha512-/faDC0erR06wMdybwI/uR8wEKV/E83T0k4sepIpB7gXuy2gzx2xiOjmztq6a2Y6rIGJ04D+6UU0VBmWy+4HEMA==", - "dev": true, - "requires": { - "@types/sinonjs__fake-timers": "*" - } - }, - "@types/sinon-chai": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", - "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, - "@types/sinonjs__fake-timers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz", - "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==", - "dev": true - }, - "@types/superagent": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.3.tgz", - "integrity": "sha512-vy2licJQwOXrTAe+yz9SCyUVXAkMgCeDq9VHzS5CWJyDU1g6CI4xKb4d5sCEmyucjw5sG0y4k2/afS0iv/1D0Q==", - "dev": true, - "requires": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, - "@types/supertest": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.8.tgz", - "integrity": "sha512-wcax7/ip4XSSJRLbNzEIUVy2xjcBIZZAuSd2vtltQfRK7kxhx5WMHbLHkYdxN3wuQCrwpYrg86/9byDjPXoGMA==", - "dev": true, - "requires": { - "@types/superagent": "*" - } - }, - "@types/tar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/tar/-/tar-4.0.3.tgz", - "integrity": "sha512-Z7AVMMlkI8NTWF0qGhC4QIX0zkV/+y0J8x7b/RsHrN0310+YNjoJd8UrApCiGBCWtKjxS9QhNqLi2UJNToh5hA==", - "dev": true, - "requires": { - "@types/minipass": "*", - "@types/node": "*" - } - }, - "@types/tcp-port-used": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/tcp-port-used/-/tcp-port-used-1.0.0.tgz", - "integrity": "sha512-UbspV5WZNhfM55HyvLEFyVc5n6K6OKuKep0mzvsgoUXQU1FS42GbePjreBnTCoKXfNzK/3/RJVCRlUDTuszFPg==", - "dev": true - }, - "@types/through": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz", - "integrity": "sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/tmp": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz", - "integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==", - "dev": true - }, - "@types/tough-cookie": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", - "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", - "dev": true - }, - "@types/triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-tl34wMtk3q+fSdRSJ+N83f47IyXLXPPuLjHm7cmAx0fE2Wml2TZCQV3FmQdSR5J6UEGV3qafG054e0cVVFCqPA==", - "dev": true - }, - "@types/unzipper": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.0.tgz", - "integrity": "sha512-GZL5vt0o9ZAST+7ge1Sirzc14EEJFbq6kib24nS0UglY6BHX8ERhA8cBq4XsYWcGK212FtMBZyJz6AwPvrhGLQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", - "dev": true - }, - "@types/winston": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", - "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==", - "dev": true, - "requires": { - "winston": "*" - } - }, - "@types/ws": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.3.tgz", - "integrity": "sha512-VT/GK7nvDA7lfHy40G3LKM+ICqmdIsBLBHGXcWD97MtqQEjNMX+7Gudo8YGpaSlYdTX7IFThhCE8Jx09HegymQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/yauzl": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", - "dev": true, - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.12.0.tgz", - "integrity": "sha512-wHKj6q8s70sO5i39H2g1gtpCXCvjVszzj6FFygneNFyIAxRvNSVz9GML7XpqrB9t7hNutXw+MHnLN/Ih6uyB8Q==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "4.12.0", - "@typescript-eslint/scope-manager": "4.12.0", - "debug": "^4.1.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/experimental-utils": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz", - "integrity": "sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.12.0", - "@typescript-eslint/types": "4.12.0", - "@typescript-eslint/typescript-estree": "4.12.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/parser": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.12.0.tgz", - "integrity": "sha512-9XxVADAo9vlfjfoxnjboBTxYOiNY93/QuvcPgsiKvHxW6tOZx1W4TvkIQ2jB3k5M0pbFP5FlXihLK49TjZXhuQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "4.12.0", - "@typescript-eslint/types": "4.12.0", - "@typescript-eslint/typescript-estree": "4.12.0", - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@typescript-eslint/scope-manager": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz", - "integrity": "sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.12.0", - "@typescript-eslint/visitor-keys": "4.12.0" - } - }, - "@typescript-eslint/types": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.12.0.tgz", - "integrity": "sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz", - "integrity": "sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.12.0", - "@typescript-eslint/visitor-keys": "4.12.0", - "debug": "^4.1.1", - "globby": "^11.0.1", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz", - "integrity": "sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.12.0", - "eslint-visitor-keys": "^2.0.0" - } - }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true - }, - "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==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true - }, - "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==", - "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "requires": { - "string-width": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "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==" - }, - "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==" - }, - "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==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansicolors": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" - }, - "anymatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.0.3.tgz", - "integrity": "sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", - "dev": true, - "requires": { - "default-require-extensions": "^3.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true - }, - "archiver": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.2.0.tgz", - "integrity": "sha512-QEAKlgQuAtUxKeZB9w5/ggKXh21bZS+dzzuQ0RPBC20qtDCbTyzqmisoeJP46MP39fg4B4IcyvR+yeyEBdblsQ==", - "requires": { - "archiver-utils": "^2.1.0", - "async": "^3.2.0", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.0.0", - "tar-stream": "^2.1.4", - "zip-stream": "^4.0.4" - }, - "dependencies": { - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "args": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", - "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", - "dev": true, - "requires": { - "camelcase": "5.0.0", - "chalk": "2.4.2", - "leven": "2.1.0", - "mri": "1.1.4" - }, - "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - } - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" - }, - "as-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", - "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "requires": { - "tslib": "^2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } - } - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atlassian-openapi": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/atlassian-openapi/-/atlassian-openapi-1.0.8.tgz", - "integrity": "sha512-aecHFJuhu5mUNdVOKbOd17+ZrCnuTw7ZFZBGaMb/fHyqUX3FEVz5e4RRgbvn1EE1+w2vmAUA+vkB9fiOzTjhQA==", - "dev": true, - "requires": { - "jsonpointer": "^4.0.1", - "urijs": "^1.18.10" - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "basic-auth-connect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", - "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=" - }, - "basic-auth-parser": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz", - "integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "better-ajv-errors": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.6.7.tgz", - "integrity": "sha512-PYgt/sCzR4aGpyNy5+ViSQ77ognMnWq7745zM+/flYO4/Yisdtp9wDQW2IKCyVYPUxQt3E/b5GBSwfhd1LPdlg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/runtime": "^7.0.0", - "chalk": "^2.4.1", - "core-js": "^3.2.1", - "json-to-ast": "^2.0.3", - "jsonpointer": "^4.0.1", - "leven": "^3.1.0" - } - }, - "big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "blakejs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", - "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U=" - }, - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, - "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "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==", - "requires": { - "color-name": "~1.1.4" - } - }, - "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==" - }, - "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==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "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==" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - }, - "dependencies": { - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - } - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "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": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-indexof-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz", - "integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=" - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" - } - } - }, - "caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", - "dev": true, - "requires": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "cardinal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", - "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", - "requires": { - "ansicolors": "~0.3.2", - "redeyed": "~2.1.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "chokidar": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.0.2.tgz", - "integrity": "sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==", - "requires": { - "anymatch": "^3.0.1", - "braces": "^3.0.2", - "fsevents": "^2.0.6", - "glob-parent": "^5.0.0", - "is-binary-path": "^2.1.0", - "is-glob": "^4.0.1", - "normalize-path": "^3.0.0", - "readdirp": "^3.1.1" - } - }, - "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", - "dev": true - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" - }, - "cjson": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", - "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", - "requires": { - "json-parse-helpfulerror": "^1.0.3" - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" - }, - "cli-color": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", - "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", - "requires": { - "ansi-regex": "^2.1.1", - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", - "timers-ext": "^0.1.5" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-spinners": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz", - "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==" - }, - "cli-table": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", - "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", - "requires": { - "colors": "1.0.3" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-error-fragment": { - "version": "0.0.230", - "resolved": "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz", - "integrity": "sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "optional": true - }, - "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", - "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colornames": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", - "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" - }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" - }, - "colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", - "requires": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", - "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==" - }, - "comment-parser": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.7.6.tgz", - "integrity": "sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-semver": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/compare-semver/-/compare-semver-1.1.0.tgz", - "integrity": "sha1-fAp5onu4C2xplERfgpWCWdPQIVM=", - "requires": { - "semver": "^5.0.1" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "compress-commons": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.2.tgz", - "integrity": "sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A==", - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", - "requires": { - "mime-db": ">= 1.40.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "requires": { - "is-obj": "^2.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" - }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "requires": { - "crypto-random-string": "^2.0.0" - } - } - } - }, - "connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true - }, - "core-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "crc-32": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", - "requires": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - } - }, - "crc32-stream": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", - "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", - "requires": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - } - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "cross-env": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", - "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", - "requires": { - "cross-spawn": "^6.0.5", - "is-windows": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - } - } - }, - "cross-spawn": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", - "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "dependencies": { - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "csv-streamify": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/csv-streamify/-/csv-streamify-3.0.4.tgz", - "integrity": "sha1-TLYUxX4/KZzKF7Y/3LStFnd39Ho=", - "requires": { - "through2": "2.0.1" - } - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" - }, - "date-and-time": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.2.tgz", - "integrity": "sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==", - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - } - } - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-freeze": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", - "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "default-require-extensions": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", - "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", - "dev": true, - "requires": { - "strip-bom": "^4.0.0" - } - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "requires": { - "clone": "^1.0.2" - } - }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" - }, - "degenerator": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", - "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", - "requires": { - "ast-types": "^0.13.2", - "escodegen": "^1.8.1", - "esprima": "^4.0.0", - "vm2": "^3.9.3" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "devtools-protocol": { - "version": "0.0.869402", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz", - "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==", - "dev": true - }, - "diagnostics": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", - "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", - "requires": { - "colorspace": "1.1.x", - "enabled": "1.0.x", - "kuler": "1.0.x" - } - }, - "dicer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", - "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", - "dev": true, - "requires": { - "streamsearch": "0.1.2" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-storage": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.1.0.tgz", - "integrity": "sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==", - "dev": true - }, - "dotenv": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", - "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "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==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "enabled": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", - "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", - "requires": { - "env-variable": "0.0.x" - } - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", - "dev": true, - "optional": true - }, - "env-paths": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", - "optional": true - }, - "env-variable": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", - "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es5-ext": { - "version": "0.10.50", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", - "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "^1.0.0" - } - }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", - "dev": true - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - } - } - }, - "eslint": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz", - "integrity": "sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.2.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^6.0.0", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.4", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - } - } - }, - "eslint-config-google": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", - "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", - "dev": true - }, - "eslint-config-prettier": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz", - "integrity": "sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA==", - "dev": true - }, - "eslint-plugin-jsdoc": { - "version": "30.7.13", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.7.13.tgz", - "integrity": "sha512-YM4WIsmurrp0rHX6XiXQppqKB8Ne5ATiZLJe2+/fkp9l9ExXFr43BbAbjZaVrpCT+tuPYOZ8k1MICARHnURUNQ==", - "dev": true, - "requires": { - "comment-parser": "^0.7.6", - "debug": "^4.3.1", - "jsdoctypeparser": "^9.0.0", - "lodash": "^4.17.20", - "regextras": "^0.7.1", - "semver": "^7.3.4", - "spdx-expression-parse": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - } - } - }, - "eslint-plugin-prettier": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", - "integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", - "dev": true - }, - "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "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==" - }, - "events-listener": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", - "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==" - }, - "exegesis": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-2.5.7.tgz", - "integrity": "sha512-Y0gEY3hgoLa80aMUm8rhhlIW3/KWo4uqN5hKJqok2GLh3maZjRLRC+p0gj33Jw3upAOKOXeRgScT5rtRoMyxwQ==", - "requires": { - "@apidevtools/json-schema-ref-parser": "^9.0.3", - "ajv": "^6.12.2", - "body-parser": "^1.18.3", - "content-type": "^1.0.4", - "deep-freeze": "0.0.1", - "events-listener": "^1.1.0", - "glob": "^7.1.3", - "json-ptr": "^2.2.0", - "json-schema-traverse": "^1.0.0", - "lodash": "^4.17.11", - "openapi3-ts": "^2.0.1", - "promise-breaker": "^5.0.0", - "pump": "^3.0.0", - "qs": "^6.6.0", - "raw-body": "^2.3.3", - "semver": "^7.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "dependencies": { - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } - } - }, - "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==" - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "exegesis-express": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-2.0.0.tgz", - "integrity": "sha512-NKvKBsBa2OvU+1BFpWbz3PzoRMhA9q7/wU2oMmQ9X8lPy/FRatADvhlkGO1zYOMgeo35k1ZLO9ZV0uIs9pPnXg==", - "requires": { - "exegesis": "^2.0.0" - } - }, - "exit-code": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/exit-code/-/exit-code-1.0.2.tgz", - "integrity": "sha1-zhZYEcnxF69qX4gpQLlq5/muzDQ=" - }, - "exit-on-epipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "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==" - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - }, - "dependencies": { - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - } - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, - "fast-text-encoding": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", - "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" - }, - "fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", - "requires": { - "punycode": "^1.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "fastq": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.0.tgz", - "integrity": "sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, - "fecha": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", - "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", - "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "file-uri-to-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", - "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==" - }, - "filesize": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", - "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==" - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "firebase": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-7.24.0.tgz", - "integrity": "sha512-j6jIyGFFBlwWAmrlUg9HyQ/x+YpsPkc/TTkbTyeLwwAJrpAmmEHNPT6O9xtAnMV4g7d3RqLL/u9//aZlbY4rQA==", - "dev": true, - "requires": { - "@firebase/analytics": "0.6.0", - "@firebase/app": "0.6.11", - "@firebase/app-types": "0.6.1", - "@firebase/auth": "0.15.0", - "@firebase/database": "0.6.13", - "@firebase/firestore": "1.18.0", - "@firebase/functions": "0.5.1", - "@firebase/installations": "0.4.17", - "@firebase/messaging": "0.7.1", - "@firebase/performance": "0.4.2", - "@firebase/polyfill": "0.3.36", - "@firebase/remote-config": "0.1.28", - "@firebase/storage": "0.3.43", - "@firebase/util": "0.3.2" - }, - "dependencies": { - "@firebase/component": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz", - "integrity": "sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ==", - "dev": true, - "requires": { - "@firebase/util": "0.3.2", - "tslib": "^1.11.1" - } - }, - "@firebase/database": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz", - "integrity": "sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA==", - "dev": true, - "requires": { - "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.19", - "@firebase/database-types": "0.5.2", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.2", - "faye-websocket": "0.11.3", - "tslib": "^1.11.1" - } - }, - "@firebase/database-types": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz", - "integrity": "sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g==", - "dev": true, - "requires": { - "@firebase/app-types": "0.6.1" - } - }, - "@firebase/util": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz", - "integrity": "sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g==", - "dev": true, - "requires": { - "tslib": "^1.11.1" - } - } - } - }, - "firebase-admin": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.4.2.tgz", - "integrity": "sha512-mRnBJbW6BAz6DJkZ0GOUTkmnmCrwVzMreMc6O+RXWukFydOzi5Xr6TKSiPKxoOQw41r9IluP2AZ3Qzvlx2SR+g==", - "dev": true, - "requires": { - "@firebase/database": "^0.8.1", - "@firebase/database-types": "^0.6.1", - "@google-cloud/firestore": "^4.5.0", - "@google-cloud/storage": "^5.3.0", - "@types/node": "^10.10.0", - "dicer": "^0.3.0", - "jsonwebtoken": "^8.5.1", - "node-forge": "^0.10.0" - }, - "dependencies": { - "@types/node": { - "version": "10.17.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.50.tgz", - "integrity": "sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA==", - "dev": true - } - } - }, - "firebase-functions": { - "version": "3.15.7", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.15.7.tgz", - "integrity": "sha512-ZD7r8eoWWebgs+mTqfH8NLUT2C0f7/cyAvIA1RSUdBVQZN7MBBt3oSlN/rL3e+m6tdlJz6YbQ3hrOKOGjOVYvQ==", - "dev": true, - "requires": { - "@types/cors": "^2.8.5", - "@types/express": "4.17.3", - "cors": "^2.8.5", - "express": "^4.17.1", - "lodash": "^4.17.14" - }, - "dependencies": { - "@types/express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", - "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" - } - } - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, - "flat-arguments": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flat-arguments/-/flat-arguments-1.0.2.tgz", - "integrity": "sha1-m6p4Ct8FAfKC1ybJxqA426ROp28=", - "requires": { - "array-flatten": "^1.0.0", - "as-array": "^1.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isobject": "^3.0.0" - }, - "dependencies": { - "as-array": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/as-array/-/as-array-1.0.0.tgz", - "integrity": "sha1-KKbu6qVynx9OyiBH316d4avaDtE=", - "requires": { - "lodash.isarguments": "2.4.x", - "lodash.isobject": "^2.4.1", - "lodash.values": "^2.4.1" - }, - "dependencies": { - "lodash.isarguments": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-2.4.1.tgz", - "integrity": "sha1-STGpwIJTrfCRrnyhkiWKlzh27Mo=" - }, - "lodash.isobject": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", - "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", - "requires": { - "lodash._objecttypes": "~2.4.1" - } - } - } - }, - "lodash.isobject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", - "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" - } - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", - "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", - "dev": true - }, - "foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", - "dev": true - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fromentries": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.1.tgz", - "integrity": "sha512-Xu2Qh8yqYuDhQGOhD5iJGninErSfI9A3FrriD3tjUgV5VbJFeH8vfgZ9HnC6jWN80QDVNQK5vmxRAmEAp7Mevw==", - "dev": true - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz", - "integrity": "sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==", - "optional": true - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "ftp": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", - "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", - "requires": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "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": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "gaxios": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", - "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - } - } - }, - "gcp-metadata": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz", - "integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==", - "requires": { - "gaxios": "^3.0.0", - "json-bigint": "^1.0.0" - }, - "dependencies": { - "bignumber.js": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", - "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" - }, - "gaxios": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", - "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - }, - "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==", - "requires": { - "bignumber.js": "^9.0.0" - } - } - } - }, - "gcs-resumable-upload": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz", - "integrity": "sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A==", - "dev": true, - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "configstore": "^5.0.0", - "extend": "^3.0.2", - "gaxios": "^3.0.0", - "google-auth-library": "^6.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - }, - "dependencies": { - "gaxios": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", - "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", - "dev": true, - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true, - "optional": true - } - } - }, - "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", - "dev": true - }, - "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==" - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "get-uri": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", - "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", - "requires": { - "@tootallnate/once": "1", - "data-uri-to-buffer": "3", - "debug": "4", - "file-uri-to-path": "2", - "fs-extra": "^8.1.0", - "ftp": "^0.3.10" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", - "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=" - }, - "glob-slasher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", - "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", - "requires": { - "glob-slash": "^1.0.0", - "lodash.isobject": "^2.4.1", - "toxic": "^1.0.0" - } - }, - "global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", - "requires": { - "ini": "^1.3.5" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", - "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "google-auth-library": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.3.tgz", - "integrity": "sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g==", - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "dependencies": { - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - } - } - }, - "google-discovery-to-swagger": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/google-discovery-to-swagger/-/google-discovery-to-swagger-2.1.0.tgz", - "integrity": "sha512-MI1gfmWPkuXCp6yH+9rfd8ZG8R1R5OIyY4WlKDTqr2+ere1gt2Ne4DSEu8HM7NkwKpuVCE5TrTRAPfm3ownMUQ==", - "dev": true, - "requires": { - "json-schema-compatibility": "^1.1.0", - "jsonpath": "^1.0.2", - "lodash": "^4.17.15", - "mime-db": "^1.21.0", - "mime-lookup": "^0.0.2", - "traverse": "~0.6.6", - "urijs": "^1.17.0" - }, - "dependencies": { - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", - "dev": true - } - } - }, - "google-gax": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.9.2.tgz", - "integrity": "sha512-Pve4osEzNKpBZqFXMfGKBbKCtgnHpUe5IQMh5Ou+Xtg8nLcba94L3gF0xgM5phMdGRRqJn0SMjcuEVmOYu7EBg==", - "requires": { - "@grpc/grpc-js": "~1.1.1", - "@grpc/proto-loader": "^0.5.1", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "google-auth-library": "^6.1.3", - "is-stream-ended": "^0.1.4", - "node-fetch": "^2.6.1", - "protobufjs": "^6.9.0", - "retry-request": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "13.13.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.36.tgz", - "integrity": "sha512-ctzZJ+XsmHQwe3xp07gFUq4JxBaRSYzKHPgblR76//UanGST7vfFNF0+ty5eEbgTqsENopzoDK090xlha9dccQ==" - }, - "protobufjs": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", - "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", - "requires": { - "@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/long": "^4.0.1", - "@types/node": "^13.7.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - } - } - } - } - }, - "google-p12-pem": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", - "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", - "requires": { - "node-forge": "^0.10.0" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" - }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "gtoken": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.1.0.tgz", - "integrity": "sha512-4d8N6Lk8TEAHl9vVoRVMh9BNOKWVgl2DdNtr3428O75r3QFrF/a5MMu851VmK0AA8+iSvbwRv69k5XnMLURGhg==", - "requires": { - "gaxios": "^4.0.0", - "google-p12-pem": "^3.0.3", - "jws": "^4.0.0", - "mime": "^2.2.0" - }, - "dependencies": { - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - } - } - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true - }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" - }, - "hash-stream-validation": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", - "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", - "dev": true, - "optional": true - }, - "hasha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", - "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", - "dev": true, - "requires": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - } - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "home-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/home-dir/-/home-dir-1.0.0.tgz", - "integrity": "sha1-KRfrRL3JByztqUJXlUOEfjAX/k4=" - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", - "dev": true - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "http2-client": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.3.tgz", - "integrity": "sha512-nUxLymWQ9pzkzTmir24p2RtsgruLmhje7lH3hLX1IpwvyTg77fW+1brenPPP3USAR+rQ36p5sTA/x7sjCJVkAA==", - "dev": true - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "agent-base": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", - "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", - "requires": { - "debug": "4" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "idb": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", - "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==", - "dev": true - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "inquirer": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", - "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.11", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - } - }, - "install-artifact-from-github": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz", - "integrity": "sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA==", - "optional": true - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" - }, - "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-stream-ended": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" - }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" - }, - "is2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", - "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", - "requires": { - "deep-is": "^0.1.3", - "ip-regex": "^2.1.0", - "is-url": "^1.2.2" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "requires": { - "append-transform": "^2.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^3.3.3" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=" - }, - "join-path": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", - "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", - "requires": { - "as-array": "^2.0.0", - "url-join": "0.0.1", - "valid-url": "^1" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "jsdoctypeparser": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz", - "integrity": "sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==", - "dev": true - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-parse-helpfulerror": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", - "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", - "requires": { - "jju": "^1.1.0" - } - }, - "json-ptr": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-2.2.0.tgz", - "integrity": "sha512-w9f6/zhz4kykltXMG7MLJWMajxiPj0q+uzQPR1cggNAE/sXoq/C5vjUb/7QNcC3rJsVIIKy37ALTXy1O+3c8QQ==", - "requires": { - "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } - } - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-compatibility": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/json-schema-compatibility/-/json-schema-compatibility-1.1.0.tgz", - "integrity": "sha1-GomBd4zaDDgYcpjZmdCJ5Rrygt8=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json-to-ast": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.1.0.tgz", - "integrity": "sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==", - "dev": true, - "requires": { - "code-error-fragment": "0.0.230", - "grapheme-splitter": "^1.0.4" - } - }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "jsonpath": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", - "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", - "dev": true, - "requires": { - "esprima": "1.2.2", - "static-eval": "2.0.2", - "underscore": "1.12.1" - }, - "dependencies": { - "esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=", - "dev": true - }, - "underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", - "dev": true - } - } - }, - "jsonpointer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", - "integrity": "sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==", - "dev": true - }, - "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", - "requires": { - "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": "^5.6.0" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "just-extend": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", - "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", - "dev": true - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "requires": { - "json-buffer": "3.0.0" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kuler": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", - "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", - "requires": { - "colornames": "^1.1.1" - } - }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "requires": { - "package-json": "^6.3.0" - } - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash._isnative": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", - "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=" - }, - "lodash._objecttypes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", - "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=" - }, - "lodash._shimkeys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", - "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", - "requires": { - "lodash._objecttypes": "~2.4.1" - } - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" - }, - "lodash.isobject": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", - "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", - "requires": { - "lodash._objecttypes": "~2.4.1" - } - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "lodash.keys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", - "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", - "requires": { - "lodash._isnative": "~2.4.1", - "lodash._shimkeys": "~2.4.1", - "lodash.isobject": "~2.4.1" - } - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, - "lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=" - }, - "lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" - }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" - }, - "lodash.values": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-2.4.1.tgz", - "integrity": "sha1-q/UUQ2s8twUAFieXjLzzCxKA7qQ=", - "requires": { - "lodash.keys": "~2.4.1" - } - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "requires": { - "chalk": "^2.0.1" - } - }, - "logform": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", - "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" - }, - "dependencies": { - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "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==", - "requires": { - "yallist": "^4.0.0" - } - }, - "lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", - "requires": { - "es5-ext": "~0.10.2" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "map-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", - "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", - "dev": true - }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" - }, - "marked-terminal": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-3.3.0.tgz", - "integrity": "sha512-+IUQJ5VlZoAFsM5MHNT7g3RHSkA3eETqhRCdXv4niUMAKHQ7lb1yvAcuGPmm4soxhmtX13u4Li6ZToXtvSEH+A==", - "requires": { - "ansi-escapes": "^3.1.0", - "cardinal": "^2.1.1", - "chalk": "^2.4.1", - "cli-table": "^0.3.1", - "node-emoji": "^1.4.1", - "supports-hyperlinks": "^1.0.1" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "memoizee": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", - "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", - "requires": { - "d": "1", - "es5-ext": "^0.10.45", - "es6-weak-map": "^2.0.2", - "event-emitter": "^0.3.5", - "is-promise": "^2.1", - "lru-queue": "0.1", - "next-tick": "1", - "timers-ext": "^0.1.5" - } - }, - "meow": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", - "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^2.5.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" - }, - "dependencies": { - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - }, - "mime-lookup": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/mime-lookup/-/mime-lookup-0.0.2.tgz", - "integrity": "sha1-o1JdJixC5MraWFmR+FADil1dJB0=", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "dependencies": { - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - } - } - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "mocha": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.1.tgz", - "integrity": "sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w==", - "dev": true, - "requires": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.4.3", - "debug": "4.2.0", - "diff": "4.0.2", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.14.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.2", - "nanoid": "3.1.12", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "7.2.0", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.0.2", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chokidar": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", - "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", - "dev": true, - "requires": { - "chalk": "^4.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - }, - "dependencies": { - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - } - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - } - } - }, - "morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "requires": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, - "mri": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", - "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", - "optional": true - }, - "nanoid": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", - "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", - "dev": true - }, - "nash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/nash/-/nash-3.0.0.tgz", - "integrity": "sha512-M5SahEycXUmko3zOvsBkF6p94CWLhnyy9hfpQ9Qzp+rQkQ8D1OaTlfTl1OBWktq9Fak3oDXKU+ev7tiMaMu+1w==", - "requires": { - "async": "^1.3.0", - "flat-arguments": "^1.0.0", - "lodash": "^4.17.5", - "minimist": "^1.1.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - } - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "nise": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", - "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^6.0.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "requires": { - "isarray": "0.0.1" - } - } - } - }, - "nock": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz", - "integrity": "sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "lodash.set": "^4.3.2", - "propagate": "^2.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "node-emoji": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", - "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", - "requires": { - "lodash.toarray": "^4.4.0" - } - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - }, - "node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "dev": true, - "requires": { - "http2-client": "^1.2.5" - } - }, - "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" - }, - "node-gyp": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", - "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", - "optional": true, - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.3", - "nopt": "^5.0.0", - "npmlog": "^4.1.2", - "request": "^2.88.2", - "rimraf": "^3.0.2", - "semver": "^7.3.2", - "tar": "^6.0.2", - "which": "^2.0.2" - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "optional": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "optional": true - }, - "minipass": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", - "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", - "optional": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "optional": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "optional": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "optional": true - } - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "optional": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "optional": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "dev": true, - "requires": { - "process-on-spawn": "^1.0.0" - } - }, - "node-readfiles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", - "integrity": "sha1-271K8SE04uY1wkXvk//Pb2BnOl0=", - "dev": true, - "requires": { - "es6-promise": "^3.2.1" - } - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "optional": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "optional": true - }, - "nyc": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", - "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", - "dev": true, - "requires": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^2.0.0", - "get-package-type": "^0.1.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "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==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "oas-kit-common": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", - "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", - "dev": true, - "requires": { - "fast-safe-stringify": "^2.0.7" - } - }, - "oas-linter": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.0.tgz", - "integrity": "sha512-LP5F1dhjULEJV5oGRg6ROztH2FddzttrrUEwq5J2GB2Zy938mg0vwt1+Rthn/qqDHtj4Qgq21duNGHh+Ew1wUg==", - "dev": true, - "requires": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - } - }, - "oas-resolver": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.3.tgz", - "integrity": "sha512-y4gP5tabqP3YcNVHNAEJAlcqZ40Y9lxemzmXvt54evbrvuGiK5dEhuE33Rf+191TOwzlxMoIgbwMYeuOM7BwjA==", - "dev": true, - "requires": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.7", - "yaml": "^1.10.0", - "yargs": "^16.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "reftools": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.7.tgz", - "integrity": "sha512-I+KZFkQvZjMZqVWxRezTC/kQ2kLhGRZ7C+4ARbgmb5WJbvFUlbrZ/6qlz6mb+cGcPNYib+xqL8kZlxCsSZ7Hew==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "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==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", - "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - } - } - }, - "oas-schema-walker": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", - "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", - "dev": true - }, - "oas-validator": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-4.0.8.tgz", - "integrity": "sha512-bIt8erTyclF7bkaySTtQ9sppqyVc+mAlPi7vPzCLVHJsL9nrivQjc/jHLX/o+eGbxHd6a6YBwuY/Vxa6wGsiuw==", - "dev": true, - "requires": { - "ajv": "^5.5.2", - "better-ajv-errors": "^0.6.7", - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.1.3", - "oas-resolver": "^2.4.3", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.5", - "should": "^13.2.1", - "yaml": "^1.8.3" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - } - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "one-time": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", - "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "open": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", - "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", - "requires": { - "is-wsl": "^1.1.0" - } - }, - "openapi-merge": { - "version": "1.0.23", - "resolved": "https://registry.npmjs.org/openapi-merge/-/openapi-merge-1.0.23.tgz", - "integrity": "sha512-5taciN3KUYFXGF3TrlO4LuPIxZW2oWMrzGrgTrO6OIW9RxCQe+Jj1xc6B3iwXdqwGeqfc4EvLFzde5++B36wQg==", - "dev": true, - "requires": { - "atlassian-openapi": "^1.0.8", - "lodash": "^4.17.15", - "ts-is-present": "^1.1.1" - } - }, - "openapi3-ts": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", - "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", - "requires": { - "yaml": "^1.10.0" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "ora": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", - "requires": { - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-spinners": "^2.0.0", - "log-symbols": "^2.2.0", - "strip-ansi": "^5.2.0", - "wcwidth": "^1.0.1" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" - }, - "p-defer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", - "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==" - }, - "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - }, - "dependencies": { - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - } - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "pac-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", - "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4", - "get-uri": "3", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "5", - "pac-resolver": "^5.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "5" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "pac-resolver": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", - "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", - "requires": { - "degenerator": "^3.0.1", - "ip": "^1.1.5", - "netmask": "^2.0.1" - } - }, - "package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - } - }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==" - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - } - } - }, - "portfinder": { - "version": "1.0.23", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.23.tgz", - "integrity": "sha512-B729mL/uLklxtxuiJKfQ84WPxNw5a7Yhx3geQZdcA4GjNjZSTSSMMWyoennMVnTWSmAR0lMdzWYN0JLnHrg1KQ==", - "requires": { - "async": "^1.5.2", - "debug": "^2.2.0", - "mkdirp": "0.5.x" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - } - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" - }, - "prettier": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", - "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "printj": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", - "dev": true, - "requires": { - "fromentries": "^1.2.0" - } - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "promise-breaker": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", - "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==" - }, - "promise-polyfill": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz", - "integrity": "sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==", - "dev": true - }, - "propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true - }, - "protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", - "requires": { - "@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/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "10.17.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.50.tgz", - "integrity": "sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA==" - } - } - }, - "proxy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.2.tgz", - "integrity": "sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==", - "dev": true, - "requires": { - "args": "5.0.1", - "basic-auth-parser": "0.0.2", - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" - } - }, - "proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", - "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", - "requires": { - "agent-base": "^6.0.0", - "debug": "4", - "http-proxy-agent": "^4.0.0", - "https-proxy-agent": "^5.0.0", - "lru-cache": "^5.1.1", - "pac-proxy-agent": "^5.0.0", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^5.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "dev": true, - "optional": true, - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", - "requires": { - "escape-goat": "^2.0.0" - } - }, - "puppeteer": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.0.0.tgz", - "integrity": "sha512-Avu8SKWQRC1JKNMgfpH7d4KzzHOL/A65jRYrjNU46hxnOYGwqe4zZp/JW8qulaH0Pnbm5qyO3EbSKvqBUlfvkg==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "devtools-protocol": "0.0.869402", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "pkg-dir": "^4.2.0", - "progress": "^2.0.1", - "proxy-from-env": "^1.1.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "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==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "re2": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/re2/-/re2-1.15.9.tgz", - "integrity": "sha512-AXWEhpMTBdC+3oqbjdU07dk0pBCvxh5vbOMLERL6Y8FYBSGn4vXlLe8cYszn64Yy7H8keVMrgPzoSvOd4mePpg==", - "optional": true, - "requires": { - "install-artifact-from-github": "^1.2.0", - "nan": "^2.14.2", - "node-gyp": "^7.1.2" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdir-glob": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", - "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "readdirp": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.1.tgz", - "integrity": "sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ==", - "requires": { - "picomatch": "^2.0.4" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "redeyed": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", - "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", - "requires": { - "esprima": "~4.0.0" - } - }, - "reftools": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.6.tgz", - "integrity": "sha512-rQfJ025lvPjw9qyQuNPqE+cRs5qVs7BMrZwgRJnmuMcX/8r/eJE8f5/RCunJWViXKHmN5K2DFafYzglLOHE/tw==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true - }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true - }, - "regextras": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz", - "integrity": "sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==", - "dev": true - }, - "registry-auth-token": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", - "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", - "requires": { - "rc": "^1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "requires": { - "rc": "^1.2.8" - } - }, - "release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "retry-request": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", - "integrity": "sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==", - "requires": { - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", - "requires": { - "glob": "^7.1.3" - } - }, - "router": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", - "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", - "requires": { - "array-flatten": "3.0.0", - "debug": "2.6.9", - "methods": "~1.1.2", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "setprototypeof": "1.2.0", - "utils-merge": "1.0.1" - }, - "dependencies": { - "array-flatten": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", - "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==" - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - } - } - }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "run-parallel": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", - "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", - "dev": true - }, - "rxjs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", - "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "should": { - "version": "13.2.3", - "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", - "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", - "dev": true, - "requires": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "should-equal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", - "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", - "dev": true, - "requires": { - "should-type": "^1.4.0" - } - }, - "should-format": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", - "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", - "dev": true, - "requires": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "should-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", - "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", - "dev": true - }, - "should-type-adaptors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", - "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", - "dev": true, - "requires": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "should-util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", - "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, - "sinon": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.3.tgz", - "integrity": "sha512-m+DyAWvqVHZtjnjX/nuShasykFeiZ+nPuEfD4G3gpvKGkXRhkF/6NSt2qN2FjZhfrcHXFzUzI+NLnk+42fnLEw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/samsam": "^5.3.0", - "diff": "^4.0.2", - "nise": "^4.0.4", - "supports-color": "^7.1.0" - }, - "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "sinon-chai": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.6.0.tgz", - "integrity": "sha512-bk2h+0xyKnmvazAnc7HE5esttqmCerSMcBtuB2PS2T4tG6x8woXAxZeJaOJWD+8reXHngnXn0RtIbfEW9OTHFg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true - } - } - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, - "snakeize": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", - "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", - "dev": true, - "optional": true - }, - "socks": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", - "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", - "requires": { - "ip": "^1.1.5", - "smart-buffer": "^4.1.0" - } - }, - "socks-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", - "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", - "requires": { - "agent-base": "^6.0.2", - "debug": "4", - "socks": "^2.3.3" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "dev": true, - "requires": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "static-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", - "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", - "dev": true, - "requires": { - "escodegen": "^1.8.1" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "dev": true, - "optional": true, - "requires": { - "stubs": "^3.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" - }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", - "dev": true - }, - "string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", - "requires": { - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - } - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", - "dev": true, - "optional": true - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "superstatic": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-7.1.0.tgz", - "integrity": "sha512-yBU8iw07nM3Bu4jFc8lnKwLey0cj61OaGmFJZcYC2X+kEpXVmXzERJ3OTAHZAESe1OTeNIuWadt81U5IULGGAA==", - "requires": { - "basic-auth-connect": "^1.0.0", - "chalk": "^1.1.3", - "compare-semver": "^1.0.0", - "compression": "^1.7.0", - "connect": "^3.6.2", - "destroy": "^1.0.4", - "fast-url-parser": "^1.1.3", - "fs-extra": "^8.1.0", - "glob-slasher": "^1.0.1", - "home-dir": "^1.0.0", - "is-url": "^1.2.2", - "join-path": "^1.1.1", - "lodash": "^4.17.19", - "mime-types": "^2.1.16", - "minimatch": "^3.0.4", - "morgan": "^1.8.2", - "nash": "^3.0.0", - "on-finished": "^2.2.0", - "on-headers": "^1.0.0", - "path-to-regexp": "^1.8.0", - "re2": "^1.15.8", - "router": "^1.3.1", - "rsvp": "^4.8.5", - "string-length": "^1.0.0", - "update-notifier": "^4.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "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==", - "requires": { - "color-name": "~1.1.4" - } - }, - "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==" - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "requires": { - "isarray": "0.0.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "update-notifier": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", - "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - } - } - }, - "supertest": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", - "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-hyperlinks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", - "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", - "requires": { - "has-flag": "^2.0.0", - "supports-color": "^5.0.0" - }, - "dependencies": { - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - } - } - }, - "swagger2openapi": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-6.2.3.tgz", - "integrity": "sha512-cUUktzLpK69UwpMbcTzjMw2ns9RZChfxh56AHv6+hTx3StPOX2foZjPgds3HlJcINbxosYYBn/D3cG8nwcCWwQ==", - "dev": true, - "requires": { - "better-ajv-errors": "^0.6.1", - "call-me-maybe": "^1.0.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.4.3", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^4.0.8", - "reftools": "^1.1.5", - "yaml": "^1.8.3", - "yargs": "^15.3.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "table": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", - "integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==", - "dev": true, - "requires": { - "ajv": "^7.0.2", - "lodash": "^4.17.20", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0" - }, - "dependencies": { - "ajv": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz", - "integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "tar": { - "version": "4.4.18", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.18.tgz", - "integrity": "sha512-ZuOtqqmkV9RE1+4odd+MhBpibmCxNP6PJhH/h2OqNuotTX7/XHPZQJv2pKvWMplFH9SIZZhitehh6vBH6LO8Pg==", - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "dependencies": { - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "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==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "tcp-port-used": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz", - "integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==", - "requires": { - "debug": "4.1.0", - "is2": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "teeny-request": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", - "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", - "dev": true, - "optional": true, - "requires": { - "http-proxy-agent": "^4.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "stream-events": "^1.0.5", - "uuid": "^8.0.0" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "optional": true - } - } - }, - "term-size": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", - "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", - "requires": { - "readable-stream": "~2.0.0", - "xtend": "~4.0.0" - }, - "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", - "requires": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "toxic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", - "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", - "requires": { - "lodash": "^4.17.10" - } - }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, - "ts-is-present": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ts-is-present/-/ts-is-present-1.1.5.tgz", - "integrity": "sha512-7cTV1I0C58HusRxMXTgbAIFu54tB+ZqGX/nf4YuePFiz40NHQbQVBgZSws1No/DJYnGf5Mx26PcyLPol01t5DQ==", - "dev": true - }, - "ts-node": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", - "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", - "dev": true, - "requires": { - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.17", - "yn": "3.1.1" - }, - "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - } - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "tsutils": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.19.0.tgz", - "integrity": "sha512-A7BaLUPvcQ1cxVu72YfD+UMI3SQPTDv/w4ol6TOwLyI0hwfG9EC+cYlhdflJTmtYTgZ3KqdPSe/otxU4K3kArg==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "tweetsodium": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/tweetsodium/-/tweetsodium-0.0.5.tgz", - "integrity": "sha512-T3aXZtx7KqQbutTtBfn+P5By3HdBuB1eCoGviIrRJV2sXeToxv2X2cv5RvYqgG26PSnN5m3fYixds22Gkfd11w==", - "requires": { - "blakejs": "^1.1.0", - "tweetnacl": "^1.0.1" - }, - "dependencies": { - "tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - } - } - }, - "type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", - "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typescript": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", - "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", - "dev": true - }, - "typescript-json-schema": { - "version": "0.50.1", - "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.50.1.tgz", - "integrity": "sha512-GCof/SDoiTDl0qzPonNEV4CHyCsZEIIf+mZtlrjoD8vURCcEzEfa2deRuxYid8Znp/e27eDR7Cjg8jgGrimBCA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.7", - "@types/node": "^14.14.33", - "glob": "^7.1.6", - "json-stable-stringify": "^1.0.1", - "ts-node": "^9.1.1", - "typescript": "~4.2.3", - "yargs": "^16.2.0" - }, - "dependencies": { - "@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", - "dev": true - }, - "@types/node": { - "version": "14.17.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz", - "integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "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==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.7", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", - "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", - "dev": true - } - } - }, - "unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "universal-analytics": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.20.tgz", - "integrity": "sha512-gE91dtMvNkjO+kWsPstHRtSwHXz0l2axqptGYp5ceg4MsuurloM0PU3pdOfpb5zBXUvyjT4PwhWK2m39uczZuw==", - "requires": { - "debug": "^3.0.0", - "request": "^2.88.0", - "uuid": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unzipper": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", - "integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==", - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "update-notifier": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", - "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", - "requires": { - "boxen": "^5.0.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.4", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "requires": { - "string-width": "^4.1.0" - } - } - } - }, - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "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==", - "requires": { - "color-name": "~1.1.4" - } - }, - "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==" - }, - "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==" - }, - "global-dirs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", - "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", - "requires": { - "ini": "2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==" - }, - "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==" - }, - "is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "requires": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - } - }, - "is-npm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", - "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==" - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "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==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - }, - "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==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "urijs": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.7.tgz", - "integrity": "sha512-Id+IKjdU0Hx+7Zx717jwLPsPeUqz7rAtuVBRLLs+qn+J2nf9NGITWVCxcijgYxBqe83C7sqsQPs6H1pyz3x9gA==", - "dev": true - }, - "url-join": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", - "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=" - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "requires": { - "prepend-http": "^2.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, - "v8-compile-cache": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", - "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", - "dev": true - }, - "valid-url": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", - "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vm2": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.5.tgz", - "integrity": "sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng==" - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "requires": { - "defaults": "^1.0.3" - } - }, - "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "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==", - "dev": true - }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "requires": { - "string-width": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "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==" - }, - "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==" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "winston": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", - "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", - "requires": { - "async": "^2.6.1", - "diagnostics": "^1.1.1", - "is-stream": "^1.1.0", - "logform": "^2.1.1", - "one-time": "0.0.4", - "readable-stream": "^3.1.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.3.0" - } - }, - "winston-transport": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", - "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", - "requires": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - }, - "workerpool": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", - "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==", - "dev": true - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" - }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", - "dev": true - }, - "xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yaml": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "dependencies": { - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true - }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - } - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - }, - "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==", - "dev": true - }, - "zip-stream": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.4.tgz", - "integrity": "sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw==", - "requires": { - "archiver-utils": "^2.1.0", - "compress-commons": "^4.0.2", - "readable-stream": "^3.6.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - } - } -} diff --git a/package.json b/package.json index 7973ef254e8..c64f12141b2 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { "name": "firebase-tools", - "version": "10.0.0", + "version": "13.12.0", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { "firebase": "./lib/bin/firebase.js" }, "scripts": { - "build": "tsc", - "build:watch": "tsc --watch", + "build": "tsc && npm run copyfiles", + "build:publish": "tsc --build tsconfig.publish.json && npm run copyfiles", + "build:watch": "npm run build && tsc --watch", "clean": "rimraf lib dev", + "copyfiles": "node -e \"const fs = require('fs'); fs.mkdirSync('./lib', {recursive:true}); fs.copyFileSync('./src/dynamicImport.js', './lib/dynamicImport.js')\"", "format": "npm run format:ts && npm run format:other", "format:other": "npm run lint:other -- --write", "format:ts": "npm run lint:ts -- --fix --quiet", @@ -20,17 +22,26 @@ "lint:other": "prettier --check '**/*.{md,yaml,yml}'", "lint:quiet": "npm run lint:ts -- --quiet && npm run lint:other", "lint:ts": "eslint --config .eslintrc.js --ext .ts,.js .", - "mocha": "nyc mocha 'src/test/**/*.{ts,js}'", - "prepare": "npm run clean && npm run build -- --build tsconfig.publish.json", + "mocha": "nyc mocha 'src/**/*.spec.{ts,js}'", + "prepare": "npm run clean && npm run build:publish", "test": "npm run lint:quiet && npm run test:compile && npm run mocha", - "test:client-integration": "./scripts/client-integration-tests/run.sh", + "test:client-integration": "bash ./scripts/client-integration-tests/run.sh", "test:compile": "tsc --project tsconfig.compile.json", - "test:emulator": "./scripts/emulator-tests/run.sh", - "test:extensions-deploy": "./scripts/extensions-deploy-tests/run.sh", - "test:extensions-emulator": "./scripts/extensions-emulator-tests/run.sh", - "test:hosting": "./scripts/hosting-tests/run.sh", - "test:triggers-end-to-end": "./scripts/triggers-end-to-end-tests/run.sh", - "test:storage-emulator-integration": "./scripts/storage-emulator-integration/run.sh" + "test:dataconnect-deploy": "bash ./scripts/dataconnect-test/run.sh", + "test:all-emulators": "npm run test:emulator && npm run test:extensions-emulator && npm run test:import-export && npm run test:storage-emulator-integration", + "test:emulator": "bash ./scripts/emulator-tests/run.sh", + "test:extensions-deploy": "bash ./scripts/extensions-deploy-tests/run.sh", + "test:extensions-emulator": "bash ./scripts/extensions-emulator-tests/run.sh", + "test:frameworks": "bash ./scripts/webframeworks-deploy-tests/run.sh", + "test:functions-deploy": "bash ./scripts/functions-deploy-tests/run.sh", + "test:functions-discover": "bash ./scripts/functions-discover-tests/run.sh", + "test:hosting": "bash ./scripts/hosting-tests/run.sh", + "test:hosting-rewrites": "bash ./scripts/hosting-tests/rewrites-tests/run.sh", + "test:import-export": "bash ./scripts/emulator-import-export-tests/run.sh", + "test:triggers-end-to-end": "bash ./scripts/triggers-end-to-end-tests/run.sh", + "test:triggers-end-to-end:inspect": "bash ./scripts/triggers-end-to-end-tests/run.sh inspect", + "test:storage-deploy": "bash ./scripts/storage-deploy-tests/run.sh", + "test:storage-emulator-integration": "bash ./scripts/storage-emulator-integration/run.sh" }, "files": [ "lib", @@ -55,7 +66,7 @@ ], "preferGlobal": true, "engines": { - "node": ">= 12" + "node": ">=18.0.0 || >=20.0.0" }, "author": "Firebase (https://firebase.google.com/)", "license": "MIT", @@ -80,138 +91,162 @@ ".ts" ], "exclude": [ + "src/**/*.spec.*", + "src/**/testing/**/*", "src/test/**/*" ] }, "dependencies": { - "@google-cloud/pubsub": "^2.7.0", - "@types/archiver": "^5.1.0", - "JSONStream": "^1.2.1", + "@google-cloud/cloud-sql-connector": "^1.2.3", + "@google-cloud/pubsub": "^4.4.0", "abort-controller": "^3.0.0", "ajv": "^6.12.6", - "archiver": "^5.0.0", + "archiver": "^7.0.0", + "async-lock": "1.3.2", "body-parser": "^1.19.0", "chokidar": "^3.0.2", "cjson": "^0.3.1", - "cli-color": "^1.2.0", "cli-table": "0.3.11", + "colorette": "^2.0.19", "commander": "^4.0.1", "configstore": "^5.0.1", "cors": "^2.8.5", "cross-env": "^5.1.3", - "cross-spawn": "^7.0.1", - "csv-streamify": "^3.0.4", - "dotenv": "^6.1.0", - "exegesis": "^2.5.7", - "exegesis-express": "^2.0.0", - "exit-code": "^1.0.2", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "deep-equal-in-any-order": "^2.0.6", + "exegesis": "^4.1.2", + "exegesis-express": "^4.0.0", "express": "^4.16.4", "filesize": "^6.1.0", - "fs-extra": "^5.0.0", - "glob": "^7.1.2", - "google-auth-library": "^6.1.3", - "inquirer": "~6.3.1", - "js-yaml": "^3.13.1", - "jsonwebtoken": "^8.5.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "fuzzy": "^0.1.3", + "gaxios": "^6.1.1", + "glob": "^10.4.1", + "google-auth-library": "^9.7.0", + "inquirer": "^8.2.6", + "inquirer-autocomplete-prompt": "^2.0.1", + "jsonwebtoken": "^9.0.0", "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", - "marked": "^0.7.0", - "marked-terminal": "^3.3.0", + "marked": "^4.0.14", + "marked-terminal": "^5.1.1", "mime": "^2.5.2", "minimatch": "^3.0.4", "morgan": "^1.10.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "open": "^6.3.0", - "ora": "^3.4.0", - "portfinder": "^1.0.23", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "pg": "^8.11.3", + "portfinder": "^1.0.32", "progress": "^2.0.3", - "proxy-agent": "^5.0.0", - "request": "^2.87.0", - "rimraf": "^3.0.0", - "semver": "^5.7.1", - "superstatic": "^7.1.0", - "tar": "^4.3.0", - "tcp-port-used": "^1.0.1", - "tmp": "0.0.33", + "proxy-agent": "^6.3.0", + "retry": "^0.13.1", + "rimraf": "^5.0.0", + "semver": "^7.5.2", + "sql-formatter": "^15.3.0", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.3", "triple-beam": "^1.3.0", - "tweetsodium": "0.0.5", - "universal-analytics": "^0.4.16", - "unzipper": "^0.10.10", - "update-notifier": "^5.1.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", "uuid": "^8.3.2", "winston": "^3.0.0", "winston-transport": "^4.4.0", - "ws": "^7.2.3" + "ws": "^7.2.3", + "yaml": "^2.4.1" }, "devDependencies": { + "@angular-devkit/architect": "^0.1402.2", + "@angular-devkit/core": "^14.2.2", "@google/events": "^5.1.1", - "@manifoldco/swagger-to-ts": "^2.0.0", + "@types/archiver": "^6.0.0", + "@types/async-lock": "^1.1.5", "@types/body-parser": "^1.17.0", - "@types/chai": "^4.2.12", - "@types/chai-as-promised": "^7.1.3", + "@types/chai": "^4.3.0", + "@types/chai-as-promised": "^7.1.4", "@types/cjson": "^0.5.0", - "@types/cli-color": "^0.3.29", "@types/cli-table": "^0.3.0", "@types/configstore": "^4.0.0", "@types/cors": "^2.8.10", "@types/cross-spawn": "^6.0.1", - "@types/dotenv": "^6.1.0", + "@types/deep-equal-in-any-order": "^1.0.3", "@types/express": "^4.17.0", "@types/express-serve-static-core": "^4.17.8", - "@types/fs-extra": "^5.0.5", - "@types/glob": "^7.1.1", - "@types/inquirer": "^6.0.3", + "@types/fs-extra": "^9.0.13", + "@types/html-escaper": "^3.0.0", + "@types/inquirer": "^8.1.3", + "@types/inquirer-autocomplete-prompt": "^2.0.2", "@types/js-yaml": "^3.12.2", - "@types/jsonwebtoken": "^8.3.8", + "@types/jsonwebtoken": "^9.0.5", + "@types/libsodium-wrappers": "^0.7.9", "@types/lodash": "^4.14.149", - "@types/marked": "^0.6.5", - "@types/mocha": "^8.2.0", + "@types/marked": "^4.0.3", + "@types/marked-terminal": "^3.1.3", + "@types/mocha": "^9.0.0", "@types/multer": "^1.4.3", - "@types/node": "^10.17.50", - "@types/node-fetch": "^2.5.7", + "@types/node": "^18.19.1", + "@types/node-fetch": "^2.5.12", + "@types/pg": "^8.11.2", "@types/progress": "^2.0.3", - "@types/puppeteer": "^5.4.2", - "@types/request": "^2.48.1", - "@types/rimraf": "^2.0.3", + "@types/react": "^18.2.58", + "@types/react-dom": "^18.2.19", + "@types/retry": "^0.12.1", "@types/semver": "^6.0.0", "@types/sinon": "^9.0.10", "@types/sinon-chai": "^3.2.2", - "@types/supertest": "^2.0.6", - "@types/tar": "^4.0.0", - "@types/tcp-port-used": "^1.0.0", - "@types/tmp": "^0.1.0", + "@types/stream-json": "^1.7.2", + "@types/supertest": "^2.0.12", + "@types/swagger2openapi": "^7.0.0", + "@types/tar": "^6.1.1", + "@types/tcp-port-used": "^1.0.1", + "@types/tmp": "^0.2.3", "@types/triple-beam": "^1.3.0", - "@types/unzipper": "^0.10.0", + "@types/universal-analytics": "^0.4.5", + "@types/update-notifier": "^5.1.0", "@types/uuid": "^8.3.1", - "@types/winston": "^2.4.4", "@types/ws": "^7.2.3", - "@typescript-eslint/eslint-plugin": "^4.12.0", - "@typescript-eslint/parser": "^4.12.0", - "chai": "^4.2.0", + "@typescript-eslint/eslint-plugin": "^5.9.0", + "@typescript-eslint/parser": "^5.9.0", + "astro": "^2.2.3", + "chai": "^4.3.4", "chai-as-promised": "^7.1.1", - "eslint": "^7.17.0", + "eslint": "^8.56.0", "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^7.1.0", - "eslint-plugin-jsdoc": "^30.7.13", - "eslint-plugin-prettier": "^3.3.1", - "firebase": "^7.24.0", - "firebase-admin": "^9.4.2", - "firebase-functions": "^3.15.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^48.0.1", + "eslint-plugin-prettier": "^5.1.3", + "firebase": "^9.16.0", + "firebase-admin": "^11.5.0", + "firebase-functions": "^4.3.1", "google-discovery-to-swagger": "^2.1.0", - "mocha": "^8.2.1", + "googleapis": "^105.0.0", + "mocha": "^9.1.3", + "next": "^14.1.0", "nock": "^13.0.5", + "node-mocks-http": "^1.11.0", "nyc": "^15.1.0", "openapi-merge": "^1.0.23", - "prettier": "^2.2.1", + "openapi-typescript": "^4.5.0", + "prettier": "^3.2.4", "proxy": "^1.0.2", - "puppeteer": "^9.0.0", + "puppeteer": "^19.0.0", "sinon": "^9.2.3", "sinon-chai": "^3.6.0", "source-map-support": "^0.5.9", - "supertest": "^3.3.0", - "swagger2openapi": "^6.0.3", - "ts-node": "^9.1.1", - "typescript": "^3.9.5", - "typescript-json-schema": "^0.50.1" + "supertest": "^6.2.3", + "swagger2openapi": "^7.0.8", + "ts-node": "^10.4.0", + "typescript": "^4.5.4", + "typescript-json-schema": "^0.50.1", + "vite": "^4.2.1" } } diff --git a/schema/connector-yaml.json b/schema/connector-yaml.json new file mode 100644 index 00000000000..a0a667c1fdc --- /dev/null +++ b/schema/connector-yaml.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "javascriptSdk": { + "additionalProperties": true, + "type": "object", + "properties": { + "outputDir": { + "type": "string", + "description": "Path to the directory where generated files should be written to." + }, + "package": { + "type": "string", + "description": "The package name to use for the generated code." + }, + "packageJSONDir": { + "type": "string", + "description": "The directory containining the package.json to install the generated package in." + } + } + }, + "kotlinSdk": { + "additionalProperties": true, + "type": "object", + "properties": { + "outputDir": { + "type": "string", + "description": "Path to the directory where generated files should be written to." + }, + "package": { + "type": "string", + "description": "The package name to use for the generated code." + } + } + }, + "swiftSdk": { + "additionalProperties": true, + "type": "object", + "properties": { + "outputDir": { + "type": "string", + "description": "Path to the directory where generated files should be written to." + } + } + } + }, + "properties": { + "connectorId": { + "type": "string", + "description": "The ID of the Firebase Data Connect connector." + }, + "authMode": { + "type": "string", + "description": "The authentication strategy to use for this connector" + + }, + "generate": { + "type": "object", + "additionalProperties": false, + "properties": { + "javascriptSdk": { + "type": "array", + "items": { + "$ref": "#/definitions/javascriptSdk" + }, + "description": "Configuration for a generated Javascript SDK" + }, + "kotlinSdk": { + "type": "array", + "items": { + "$ref": "#/definitions/kotlinSdk" + }, + "description": "Configuration for a generated Kotlin SDK" + }, + "swiftSdk": { + "type": "array", + "items": { + "$ref": "#/definitions/swiftSdk" + }, + "description": "Configuration for a generated Swift SDK" + } + } + } + } +} diff --git a/schema/dataconnect-yaml.json b/schema/dataconnect-yaml.json new file mode 100644 index 00000000000..9d4e63a23a6 --- /dev/null +++ b/schema/dataconnect-yaml.json @@ -0,0 +1,76 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "postgresql": { + "additionalProperties": false, + "type": "object", + "properties": { + "database": { + "type": "string", + "description": "The name of the PostgreSQL database." + }, + "cloudSql": { + "additionalProperties": false, + "type": "object", + "properties": { + "instanceId": { + "type": "string", + "description": "The ID of the CloudSQL instance for this database" + } + } + } + } + }, + "dataSource": { + "oneOf": [ + { + "additionalProperties": false, + "type": "object", + "properties": { + "postgresql": { + "$ref": "#/definitions/postgresql" + } + } + } + ] + }, + "schema": { + "additionalProperties": false, + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "Relative path to directory containing GQL files defining the schema. If omitted, defaults to ./schema." + }, + "datasource": { + "$ref": "#/definitions/dataSource" + } + } + } + }, + "properties": { + "specVersion": { + "type": "string", + "description": "The Firebase Data Connect API version to target. If omitted, defaults to the latest version" + }, + "serviceId": { + "type": "string", + "description": "The ID of the Firebase Data Connect service." + }, + "location": { + "type": "string", + "description": "The region of the Firebase Data Connect service." + }, + "connectorDirs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of directories containing conector.yaml files describing a connector to deploy." + }, + "schema": { + "$ref": "#/definitions/schema" + } + } +} diff --git a/schema/extension-yaml.json b/schema/extension-yaml.json new file mode 100644 index 00000000000..6440c59e937 --- /dev/null +++ b/schema/extension-yaml.json @@ -0,0 +1,432 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "author": { + "additionalProperties": false, + "type": "object", + "properties": { + "authorName": { + "type": "string", + "description": "The author's name" + }, + "email": { + "type": "string", + "description": "A contact email for the author" + }, + "url": { + "type": "string", + "description": "URL of the author's website" + } + } + }, + "role": { + "additionalProperties": false, + "type": "object", + "description": "An IAM role to grant to this extension.", + "properties": { + "role": { + "type": "string", + "description": "Name of the IAM role to grant. Must be on the list of allowed roles: https://firebase.google.com/docs/extensions/publishers/access#supported-roles", + "pattern": "[a-zA-Z]+\\.[a-zA-Z]+" + }, + "reason": { + "type": "string", + "description": "Why this extension needs this IAM role" + }, + "resource": { + "type": "string", + "description": "What resource to grant this role on. If omitted, defaults to projects/${project_id}" + } + }, + "required": ["role", "reason"] + }, + "api": { + "additionalProperties": false, + "type": "object", + "description": "A Google API used by this extension. Will be enabled on extension deployment.", + "properties": { + "apiName": { + "type": "string", + "description": "Name of the Google API to enable. Should match the service name listed in https://console.cloud.google.com/apis/library", + "pattern": "[^\\.]+\\.googleapis\\.com" + }, + "reason": { + "type": "string", + "description": "Why this extension needs this API enabled" + } + }, + "required": ["apiName", "reason"] + }, + "externalService": { + "additionalProperties": false, + "type": "object", + "description": "A non-Google API used by this extension", + "properties": { + "name": { + "type": "string", + "description": "Name of the external service" + }, + "pricingUri": { + "type": "string", + "description": "URI to pricing information for the service" + } + } + }, + "param": { + "additionalProperties": false, + "type": "object", + "description": "A parameter that users installing this extension can configure", + "properties": { + "param": { + "type": "string", + "description": "The name of the param. This is how you reference the param in your code" + }, + "label": { + "type": "string", + "description": "Short description for the parameter. Displayed to users when they're prompted for the parameter's value." + }, + "description": { + "type": "string", + "description": "Detailed description for the parameter. Displayed to users when they're prompted for the parameter's value." + }, + "example": { + "type": "string", + "description": "Example value for the parameter." + }, + "validationRegex": { + "type": "string", + "description": "Regular expression for validation of the parameter's user-configured value. Uses Google RE2 syntax." + }, + "validationErrorMessage": { + "type": "string", + "description": "Error message to display if regex validation fails." + }, + "default": { + "type": "string", + "description": "Default value for the parameter if the user leaves the parameter's value blank." + }, + "required": { + "type": "boolean", + "description": "Defines whether the user can submit an empty string when they're prompted for the parameter's value. Defaults to true." + }, + "immutable": { + "type": "boolean", + "description": "Defines whether the user can change the parameter's value after installation (such as if they reconfigure the extension). Defaults to false." + }, + "advanced": { + "type": "boolean", + "description": "Whether this a param for advanced users. When true, only users who choose 'advanced configuration' will see this param." + }, + "type": { + "type": "string", + "description": "The parameter type. Special parameter types might have additional requirements or different UI presentation. See https://firebase.google.com/docs/extensions/reference/extension-yaml#params for more details.", + "pattern": "string|STRING|select|SELECT|multiselect|MULTISELECT|secret|SECRET|selectresource|SELECTRESOURCE" + }, + "resourceType": { + "type": "string", + "description": "The type of resource to prompt the user to select. Provides a special UI treatment for the param.", + "pattern": "storage\\.googleapis\\.com\\/Bucket|firestore\\.googleapis\\.com\\/Database|firebasedatabase\\.googleapis\\.com\\/DatabaseInstance" + }, + "options": { + "type": "array", + "description": "Options for a select or multiselect type param.", + "items": { + "$ref": "#/definitions/paramOption" + } + } + }, + "required": ["param"] + }, + "paramOption": { + "additionalProperties": false, + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "One of the values the user can choose. This is the value you get when you read the parameter value in code." + }, + "label": { + "type": "string", + "description": "Short description of the selectable option. If omitted, defaults to value." + } + }, + "required": ["value"] + }, + "resource":{ + "additionalProperties": false, + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of this resource" + }, + "type": { + "type": "string", + "description": "What type of resource this is. See https://firebase.google.com/docs/extensions/reference/extension-yaml#resources for a full list of options." + }, + "description": { + "type": "string", + "description": "A brief description of what this resource does" + }, + "properties": { + "type": "object", + "description": "The properties of this resource", + "additionalProperties": true, + "properties": { + "location": { + "type": "string", + "description": "The location for this resource" + }, + "entryPoint": { + "type": "string", + "description": "The entry point for a function resource" + }, + "sourceDirectory": { + "type": "string", + "description": "Directory that contains your package.json at its root. The file for your functions source code must be in this directory. Defaults to functions" + }, + "timeout": { + "type": "string", + "description": "A function resources's maximum execution time.", + "pattern": "\\d+s" + }, + "availableMemoryMb": { + "type": "string", + "description": "Amount of memory in MB available for the function.", + "pattern": "\\d+" + }, + "runtime": { + "type": "string", + "description": "Runtime environment for the function. Defaults to the most recent LTS version of node." + }, + "httpsTrigger": { + "type": "object", + "description": "A function triggered by HTTPS calls", + "properties": {} + }, + "eventTrigger": { + "type": "object", + "description": "A function triggered by a background event", + "properties": { + "eventType": { + "type": "string", + "description": "The type of background event to trigger on. See https://firebase.google.com/docs/extensions/publishers/functions#supported for a full list." + }, + "resource": { + "type": "string", + "description": "The name or pattern of the resource to trigger on" + }, + "eventFilters": { + "type": "string", + "description": "Filters that further limit the events to listen to." + }, + "channel": { + "type": "string", + "description": "The name of the channel associated with the trigger in projects/{project}/locations/{location}/channels/{channel} format. If you omit this property, the function will listen for events on the project's default channel." + }, + "triggerRegion": { + "type": "string", + "description": "The trigger will only receive events originating in this region. It can be the same region as the function, a different region or multi-region, or the global region. If not provided, defaults to the same region as the function." + } + }, + "required": ["eventType"] + }, + "scheduleTrigger": { + "type": "object", + "description": "A function triggered at a regular interval by a Cloud Scheduler job", + "properties": { + "schedule": { + "type": "string", + "description": "The frequency at which you want the function to run. Accepts unix-cron (https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules) or App Engine (https://cloud.google.com/appengine/docs/standard/nodejs/scheduling-jobs-with-cron-yaml#defining_the_cron_job_schedule) syntax." + }, + "timeZone": { + "type": "string", + "description": "The time zone in which the schedule will run. Defaults to UTC." + } + }, + "required": ["schedule"] + }, + "taskQueueTrigger": { + "type": "object", + "description": "A function triggered by a Cloud Task", + "properties": {} + }, + "buildConfig": { + "type": "object", + "description": "Build configuration for a gen 2 Cloud Function", + "properties": { + "runtime": { + "type": "string", + "description": "Runtime environment for the function. Defaults to the most recent LTS version of node." + }, + "entryPoint": { + "type": "string", + "description": "The entry point for a function resource" + } + } + }, + "serviceConfig": { + "type": "object", + "description": "Service configuration for a gen 2 Cloud Function", + "properties": { + "timeoutSeconds": { + "type": "string", + "description": "The function's maximum execution time. Default: 60, max value: 540." + }, + "availableMemory": { + "type": "string", + "description": "The amount of memory available for a function. Defaults to 256M. Supported units are k, M, G, Mi, Gi. If no unit is supplied, the value is interpreted as bytes." + } + } + } + } + } + }, + "required": ["name", "type", "description", "properties"] + }, + "lifecycleEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "onInstall": { + "$ref": "#/definitions/lifecycleEventSpec" + }, + "onUpdate": { + "$ref": "#/definitions/lifecycleEventSpec" + }, + "onConfigure": { + "$ref": "#/definitions/lifecycleEventSpec" + } + } + }, + "lifecycleEventSpec": { + "type": "object", + "additionalProperties": false, + "properties": { + "function": { + "type": "string", + "description": "Name of the task queue-triggered function that will handle the event. This function must be a taskQueueTriggered function declared in the resources section." + }, + "processingMessage": { + "type": "string", + "description": "Message to display in the Firebase console while the task is in progress." + } + } + }, + "event": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "The type identifier of the event. Construct the identifier out of 3-4 dot-delimited fields: the publisher ID, extension name, and event name fields are required; the version field is recommended. Choose a unique and descriptive event name for each event type you publish." + }, + "description": { + "type": "string", + "description": "A description of the event" + } + } + } + }, + "properties": { + "name": { + "type": "string", + "description": "ID of this extension (ie your-extension-name)" + }, + "version": { + "type": "string", + "description": "Version of this extension. Follows https://semver.org/." + }, + "specVersion": { + "type":"string", + "description": "Version of the extension.yaml spec that this file follows. Currently always 'v1beta'" + }, + "license": { + "type": "string", + "description": "The software license agreement for this extension. Currently, only 'Apache-2.0' is permitted on extensions.dev" + }, + "displayName": { + "type": "string", + "description": "Human readable name for this extension (ie 'Your Extension Name')" + }, + "description": { + "type": "string", + "description": "A one to two sentence description of what this extension does" + }, + "icon": { + "type": "string", + "description": "The file name of this extension's icon" + }, + "billingRequired": { + "type": "boolean", + "description": "Whether this extension requires a billing to be enabled on the project it is installed on" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of tags to help users find your extension in search" + }, + "sourceUrl": { + "type": "string", + "description": "The URL of the GitHub repo hosting this code" + }, + "releaseNotesUrl": { + "type": "string", + "description": "A URL where users can view the full changelog or release notes for this extension" + }, + "author": { + "$ref": "#/definitions/author" + }, + "contributors": { + "type": "array", + "items": { + "$ref": "#/definitions/author" + } + }, + "apis": { + "type": "array", + "items": { + "$ref": "#/definitions/api" + } + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/definitions/role" + } + }, + "externalServices": { + "type": "array", + "items": { + "$ref": "#/definitions/externalService" + } + }, + "params": { + "type": "array", + "items": { + "$ref": "#/definitions/param" + } + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/definitions/resource" + } + }, + "lifecycleEvents": { + "type": "array", + "items": { + "$ref": "#/definitions/lifecycleEvent" + } + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/event" + } + } + } +} diff --git a/schema/firebase-config.json b/schema/firebase-config.json index d1da4d7447c..7ef9a598ffe 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -5,9 +5,133 @@ "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": [ { @@ -147,6 +271,89 @@ } ] }, + "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": { @@ -174,6 +381,35 @@ }, "type": "object" }, + "dataconnect": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "eventarc": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "extensions": { + "properties": { + }, + "type": "object" + }, "firestore": { "additionalProperties": false, "properties": { @@ -182,6 +418,9 @@ }, "port": { "type": "number" + }, + "websocketPort": { + "type": "number" } }, "type": "object" @@ -246,6 +485,9 @@ }, "type": "object" }, + "singleProjectMode": { + "type": "boolean" + }, "storage": { "additionalProperties": false, "properties": { @@ -283,92 +525,279 @@ "$ref": "#/definitions/ExtensionsConfig" }, "firestore": { - "additionalProperties": false, - "properties": { - "indexes": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "database": { "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" }, - { + "indexes": { "type": "string" - } - ] - }, - "rules": { - "type": "string" - } - }, - "type": "object" - }, - "functions": { - "additionalProperties": false, - "properties": { - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] }, - { + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { "type": "string" } - ] + }, + "type": "object" }, - "runtime": { - "enum": [ - "nodejs10", - "nodejs12", - "nodejs14", - "nodejs16" - ], - "type": "string" + { + "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" }, - "source": { - "type": "string" + { + "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" } - }, - "type": "object" + ] }, "hosting": { "anyOf": [ @@ -376,14 +805,53 @@ "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": { @@ -489,36 +957,193 @@ "items": { "type": "string" }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "public": { - "type": "string" - }, - "redirects": { - "items": { - "anyOf": [ + "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": { - "destination": { - "type": "string" + "dynamicLinks": { + "type": "boolean" }, - "source": { + "glob": { "type": "string" - }, - "type": { - "type": "number" } }, "required": [ - "destination", - "source", - "type" + "dynamicLinks", + "glob" ], "type": "object" }, @@ -528,31 +1153,23 @@ "destination": { "type": "string" }, - "regex": { + "source": { "type": "string" - }, - "type": { - "type": "number" } }, "required": [ "destination", - "regex", - "type" + "source" ], "type": "object" - } - ] - }, - "type": "array" - }, - "rewrites": { - "items": { - "anyOf": [ + }, { "additionalProperties": false, "properties": { - "destination": { + "function": { + "type": "string" + }, + "region": { "type": "string" }, "source": { @@ -560,7 +1177,7 @@ } }, "required": [ - "destination", + "function", "source" ], "type": "object" @@ -569,7 +1186,22 @@ "additionalProperties": false, "properties": { "function": { - "type": "string" + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" }, "source": { "type": "string" @@ -587,6 +1219,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -647,6 +1282,40 @@ "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" } @@ -666,6 +1335,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -708,6 +1380,9 @@ "site": { "type": "string" }, + "source": { + "type": "string" + }, "target": { "type": "string" }, @@ -724,14 +1399,53 @@ "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": { @@ -850,6 +1564,25 @@ "redirects": { "items": { "anyOf": [ + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "glob": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": [ + "destination", + "glob" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -865,8 +1598,7 @@ }, "required": [ "destination", - "source", - "type" + "source" ], "type": "object" }, @@ -885,8 +1617,7 @@ }, "required": [ "destination", - "regex", - "type" + "regex" ], "type": "object" } @@ -897,6 +1628,119 @@ "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": { @@ -917,7 +1761,41 @@ "additionalProperties": false, "properties": { "function": { - "type": "string" + "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" @@ -935,6 +1813,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -995,6 +1876,40 @@ "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" } @@ -1014,6 +1929,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1056,6 +1974,9 @@ "site": { "type": "string" }, + "source": { + "type": "string" + }, "target": { "type": "string" }, @@ -1072,14 +1993,53 @@ "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": { @@ -1198,6 +2158,25 @@ "redirects": { "items": { "anyOf": [ + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "glob": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": [ + "destination", + "glob" + ], + "type": "object" + }, { "additionalProperties": false, "properties": { @@ -1213,8 +2192,7 @@ }, "required": [ "destination", - "source", - "type" + "source" ], "type": "object" }, @@ -1233,8 +2211,7 @@ }, "required": [ "destination", - "regex", - "type" + "regex" ], "type": "object" } @@ -1245,6 +2222,119 @@ "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": { @@ -1267,6 +2357,40 @@ "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" } @@ -1283,6 +2407,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1343,6 +2470,40 @@ "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" } @@ -1362,6 +2523,9 @@ "run": { "additionalProperties": false, "properties": { + "pinTag": { + "type": "boolean" + }, "region": { "type": "string" }, @@ -1404,6 +2568,9 @@ "site": { "type": "string" }, + "source": { + "type": "string" + }, "target": { "type": "string" }, diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index 34ca05ae257..1ab5d0253df 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -1,9 +1,14 @@ -FROM node:12 +FROM node:20 # Install dependencies +RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - +RUN echo "deb https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list RUN apt-get update && \ - apt-get install -y curl git jq + apt-get install -y curl git jq google-cloud-cli # Install hub RUN curl -fsSL --output hub.tgz https://github.com/github/hub/releases/download/v2.14.2/hub-linux-amd64-2.14.2.tgz RUN tar --strip-components=2 -C /usr/bin -xf hub.tgz hub-linux-amd64-2.14.2/bin/hub + +# Upgrade npm to 9. +RUN npm install --global npm@9.5 diff --git a/scripts/clean-install.sh b/scripts/clean-install.sh new file mode 100755 index 00000000000..a33539ef86d --- /dev/null +++ b/scripts/clean-install.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +function cleanup() { + echo "Cleaning up artifacts..." + rm -rf ./clean + echo "Artifacts deleted." +} + +trap cleanup EXIT + +rm -rf ./clean || true +echo "Running clean-publish --without-publish, as we would before publishing to npm..." +npx --yes clean-publish --without-publish --before-script ./scripts/clean-shrinkwrap.sh --temp-dir clean +echo "Ran clean-publish --without-publish." +echo "Packaging cleaned firebase-tools..." +cd ./clean +PACKED=$(npm pack --pack-destination ./ | tail -n 1) +echo "Packaged firebase-tools to $PACKED." +echo "Installing clean-packaged firebase-tools..." +npm install -g $PACKED +echo "Installed clean-packaged firebase-tools." diff --git a/scripts/clean-shrinkwrap.sh b/scripts/clean-shrinkwrap.sh new file mode 100755 index 00000000000..d0a3ce02e1e --- /dev/null +++ b/scripts/clean-shrinkwrap.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +npx ts-node ./scripts/clean-shrinkwrap.ts "$1" diff --git a/scripts/clean-shrinkwrap.ts b/scripts/clean-shrinkwrap.ts new file mode 100644 index 00000000000..abbfc54f10f --- /dev/null +++ b/scripts/clean-shrinkwrap.ts @@ -0,0 +1,28 @@ +import { readFileSync, writeFileSync } from "fs"; +import { resolve } from "path"; + +const tmpDir = process.argv[2]; +const file = resolve(__dirname, "..", tmpDir, "npm-shrinkwrap.json"); + +const shrinkwrapStr = readFileSync(file, "utf8"); +const shrinkwrap = JSON.parse(shrinkwrapStr); + +shrinkwrap.packages[""].devDependencies = {}; + +const newPkgs: Record = {}; +for (const [pkg, info] of Object.entries(shrinkwrap.packages)) { + if (!info.dev) { + newPkgs[pkg] = info; + } +} +shrinkwrap.packages = newPkgs; + +const newDependencies: Record = {}; +for (const [pkg, info] of Object.entries(shrinkwrap.dependencies)) { + if (!info.dev) { + newDependencies[pkg] = info; + } +} +shrinkwrap.dependencies = newDependencies; + +writeFileSync(file, JSON.stringify(shrinkwrap, undefined, 2)); diff --git a/scripts/client-integration-tests/run.sh b/scripts/client-integration-tests/run.sh index 42dba1ee2d5..a46669d2bea 100755 --- a/scripts/client-integration-tests/run.sh +++ b/scripts/client-integration-tests/run.sh @@ -2,8 +2,4 @@ source scripts/set-default-credentials.sh -mocha \ - --require ts-node/register \ - --require source-map-support/register \ - --require src/test/helpers/mocha-bootstrap.ts \ - scripts/client-integration-tests/tests.ts \ No newline at end of file +mocha scripts/client-integration-tests/tests.ts \ No newline at end of file diff --git a/scripts/client-integration-tests/tests.ts b/scripts/client-integration-tests/tests.ts index a8ef603f47c..8630d1cc93b 100644 --- a/scripts/client-integration-tests/tests.ts +++ b/scripts/client-integration-tests/tests.ts @@ -98,7 +98,7 @@ describe("database:set|get|remove", () => { await client.database.set( path, - Object.assign({ data: JSON.stringify(data), force: true }, opts) + Object.assign({ data: JSON.stringify(data), force: true }, opts), ); // Have to read to a file in order to get data. diff --git a/scripts/dataconnect-test/.firebaserc b/scripts/dataconnect-test/.firebaserc new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/scripts/dataconnect-test/.firebaserc @@ -0,0 +1 @@ +{} diff --git a/scripts/dataconnect-test/.gitignore b/scripts/dataconnect-test/.gitignore new file mode 100644 index 00000000000..dbb58ffbfa3 --- /dev/null +++ b/scripts/dataconnect-test/.gitignore @@ -0,0 +1,66 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env diff --git a/scripts/dataconnect-test/fdc-test/connector/connector.yaml b/scripts/dataconnect-test/fdc-test/connector/connector.yaml new file mode 100644 index 00000000000..68215053ca4 --- /dev/null +++ b/scripts/dataconnect-test/fdc-test/connector/connector.yaml @@ -0,0 +1,2 @@ +connectorId: "connectorId" +authMode: "PUBLIC" diff --git a/scripts/dataconnect-test/fdc-test/connector/mutations.gql b/scripts/dataconnect-test/fdc-test/connector/mutations.gql new file mode 100644 index 00000000000..51e05570ca5 --- /dev/null +++ b/scripts/dataconnect-test/fdc-test/connector/mutations.gql @@ -0,0 +1,3 @@ +mutation createOrder($name: String!) { + order_insert(data : {name: $name}) +} diff --git a/scripts/dataconnect-test/fdc-test/dataconnect.yaml b/scripts/dataconnect-test/fdc-test/dataconnect.yaml new file mode 100644 index 00000000000..09b3b25fb8a --- /dev/null +++ b/scripts/dataconnect-test/fdc-test/dataconnect.yaml @@ -0,0 +1,10 @@ +specVersion: "v1alpha" +serviceId: "integration-test" +schema: + source: "./schema" + datasource: + postgresql: + database: "dataconnect-test" + cloudSql: + instanceId: "dataconnect-test" +connectorDirs: ["./connector"] diff --git a/scripts/dataconnect-test/fdc-test/schema/schema.gql b/scripts/dataconnect-test/fdc-test/schema/schema.gql new file mode 100644 index 00000000000..49fe95f27db --- /dev/null +++ b/scripts/dataconnect-test/fdc-test/schema/schema.gql @@ -0,0 +1,14 @@ +type Product @table { + name: String! + price: Int! +} + +type Order @table { + name: String! +} + +type OrderItem @table(key: ["order", "product"]) { + order: Order! + product: Product! + quantity: Int! +} \ No newline at end of file diff --git a/scripts/dataconnect-test/firebase.json b/scripts/dataconnect-test/firebase.json new file mode 100644 index 00000000000..2b6aad2c7c9 --- /dev/null +++ b/scripts/dataconnect-test/firebase.json @@ -0,0 +1,6 @@ +{ + "dataconnect": { + "source": "fdc-test", + "location": "us-central1" + } +} diff --git a/scripts/dataconnect-test/run.sh b/scripts/dataconnect-test/run.sh new file mode 100644 index 00000000000..de0ecb0555f --- /dev/null +++ b/scripts/dataconnect-test/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Globally link the CLI for the testing framework +./scripts/clean-install.sh + +mocha scripts/dataconnect-test/tests.ts +rm -rf ../../clean \ No newline at end of file diff --git a/scripts/dataconnect-test/tests.ts b/scripts/dataconnect-test/tests.ts new file mode 100644 index 00000000000..a0746a5f403 --- /dev/null +++ b/scripts/dataconnect-test/tests.ts @@ -0,0 +1,72 @@ +import * as cli from "../functions-deploy-tests/cli"; +import { expect } from "chai"; + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; +const expected = { + serviceId: "integration-test", + location: "us-central1", + datasource: "CloudSQL Instance: dataconnect-test\nDatabase:dataconnect-test", + schemaUpdateTime: "", + connectors: [ + { + connectorId: "connectorId", + connectorLastUpdated: "", + }, + ], +}; + +async function list() { + return await cli.exec( + "dataconnect:services:list", + FIREBASE_PROJECT, + ["--json"], + __dirname, + /** quiet=*/ false, + { + FIREBASE_CLI_EXPERIMENTS: "dataconnect", + }, + ); +} + +async function migrate() { + return await cli.exec( + "dataconnect:sql:migrate", + FIREBASE_PROJECT, + ["--force"], + __dirname, + /** quiet=*/ false, + { FIREBASE_CLI_EXPERIMENTS: "dataconnect" }, + ); +} + +async function deploy() { + return await cli.exec( + "deploy", + FIREBASE_PROJECT, + ["--only", "dataconnect", "--force"], + __dirname, + /** quiet=*/ false, + { FIREBASE_CLI_EXPERIMENTS: "dataconnect" }, + ); +} + +describe("firebase deploy", () => { + before(() => { + expect(FIREBASE_PROJECT).not.to.equal("", "No FBTOOLS_TARGET_PROJECT env var set."); + }); + + it("should deploy expected connectors and services", async () => { + await migrate(); + await deploy(); + + const result = await list(); + const out = JSON.parse(result.stdout); + expect(out?.status).to.equal("success"); + expect(out?.result?.services?.length).to.gt(1); + const service = out.result.services.find((s: any) => s.serviceId === "integration-test"); + // Don't need to check update times. + expected.schemaUpdateTime = service["schemaUpdateTime"]; + expected.connectors[0].connectorLastUpdated = service["connectors"][0]["connectorLastUpdated"]; + expect(service).to.deep.equal(expected); + }).timeout(2000000); // Insanely long timeout in case of cSQL deploy. Should almost never be hit. +}); diff --git a/scripts/emulator-import-export-tests/.firebaserc b/scripts/emulator-import-export-tests/.firebaserc new file mode 100644 index 00000000000..f7b55c6f220 --- /dev/null +++ b/scripts/emulator-import-export-tests/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "fir-tools-testing" + } +} diff --git a/scripts/emulator-import-export-tests/.gitignore b/scripts/emulator-import-export-tests/.gitignore new file mode 100644 index 00000000000..17731ad4795 --- /dev/null +++ b/scripts/emulator-import-export-tests/.gitignore @@ -0,0 +1,71 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +database-debug.log* +firestore-debug.log* +pubsub-debug.log* + +# NPM +package-lock.json + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env \ No newline at end of file diff --git a/scripts/emulator-import-export-tests/firebase.json b/scripts/emulator-import-export-tests/firebase.json new file mode 100644 index 00000000000..e37ee130128 --- /dev/null +++ b/scripts/emulator-import-export-tests/firebase.json @@ -0,0 +1,47 @@ +{ + "database": {}, + "firestore": { + "rules": "firestore.rules" + }, + "storage": { + "rules": "storage.rules" + }, + "functions": [ + { + "codebase": "triggers", + "source": "triggers" + }, + { + "codebase": "v1", + "source": "v1" + }, + { + "codebase": "v2", + "source": "v2" + } + + ], + "emulators": { + "hub": { + "port": 4000 + }, + "database": { + "port": 9000 + }, + "firestore": { + "port": 9001 + }, + "functions": { + "port": 9002 + }, + "pubsub": { + "port": 8085 + }, + "auth": { + "port": 9099 + }, + "storage": { + "port": 9199 + } + } +} diff --git a/scripts/emulator-import-export-tests/run.sh b/scripts/emulator-import-export-tests/run.sh new file mode 100755 index 00000000000..96efb4e8edd --- /dev/null +++ b/scripts/emulator-import-export-tests/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +source scripts/set-default-credentials.sh +./scripts/clean-install.sh + +npx mocha --exit scripts/emulator-import-export-tests/tests.ts \ No newline at end of file diff --git a/scripts/emulator-import-export-tests/storage.rules b/scripts/emulator-import-export-tests/storage.rules new file mode 100644 index 00000000000..a7db6961cad --- /dev/null +++ b/scripts/emulator-import-export-tests/storage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if true; + } + } +} diff --git a/scripts/emulator-import-export-tests/tests.ts b/scripts/emulator-import-export-tests/tests.ts new file mode 100644 index 00000000000..6ed4133b637 --- /dev/null +++ b/scripts/emulator-import-export-tests/tests.ts @@ -0,0 +1,577 @@ +import { expect } from "chai"; +import * as admin from "firebase-admin"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +import { CLIProcess } from "../integration-helpers/cli"; +import { FrameworkOptions } from "../integration-helpers/framework"; +import { Resolver } from "../../src/emulator/dns"; + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; +const ADMIN_CREDENTIAL = { + getAccessToken: () => { + return Promise.resolve({ + expires_in: 1000000, + access_token: "owner", + }); + }, +}; + +const ALL_EMULATORS_STARTED_LOG = "All emulators ready"; + +/* + * Various delays that are needed because this test spawns + * parallel emulator subprocesses. + */ +const TEST_SETUP_TIMEOUT = 60000; + +const r = new Resolver(); +let addr: string; +async function localhost(): Promise { + if (addr) { + return addr; + } + const a = await r.lookupFirst("localhost"); + addr = a.address; + return addr; +} + +function readConfig(): FrameworkOptions { + const filename = path.join(__dirname, "firebase.json"); + const data = fs.readFileSync(filename, "utf8"); + return JSON.parse(data); +} + +function logIncludes(msg: string) { + return (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(msg); + }; +} + +describe("import/export end to end", () => { + it("should be able to import/export firestore data", async function (this) { + this.timeout(2 * TEST_SETUP_TIMEOUT); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Start up emulator suite + const emulatorsCLI = new CLIProcess("1", __dirname); + await emulatorsCLI.start( + "emulators:start", + FIREBASE_PROJECT, + ["--only", "firestore", "--debug"], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }, + ); + + // Ask for export + const exportCLI = new CLIProcess("2", __dirname); + const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); + await exportCLI.start("emulators:export", FIREBASE_PROJECT, [exportPath], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes("Export complete"); + }); + await exportCLI.stop(); + + // Stop the suite + await emulatorsCLI.stop(); + + // Attempt to import + const importCLI = new CLIProcess("3", __dirname); + await importCLI.start( + "emulators:start", + FIREBASE_PROJECT, + ["--only", "firestore", "--import", exportPath], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }, + ); + + await importCLI.stop(); + + expect(true).to.be.true; + }); + + it("should be able to import/export rtdb data", async function (this) { + this.timeout(2 * TEST_SETUP_TIMEOUT); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Start up emulator suite + const emulatorsCLI = new CLIProcess("1", __dirname); + await emulatorsCLI.start( + "emulators:start", + FIREBASE_PROJECT, + ["--only", "database"], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }, + ); + + // Write some data to export + const config = readConfig(); + const port = config.emulators!.database.port; + const host = await localhost(); + const aApp = admin.initializeApp( + { + projectId: FIREBASE_PROJECT, + databaseURL: `http://${host}:${port}?ns=namespace-a`, + credential: ADMIN_CREDENTIAL, + }, + "rtdb-export-a", + ); + const bApp = admin.initializeApp( + { + projectId: FIREBASE_PROJECT, + databaseURL: `http://${host}:${port}?ns=namespace-b`, + credential: ADMIN_CREDENTIAL, + }, + "rtdb-export-b", + ); + const cApp = admin.initializeApp( + { + projectId: FIREBASE_PROJECT, + databaseURL: `http://${host}:${port}?ns=namespace-c`, + credential: ADMIN_CREDENTIAL, + }, + "rtdb-export-c", + ); + + // Write to two namespaces + const aRef = aApp.database().ref("ns"); + await aRef.set("namespace-a"); + const bRef = bApp.database().ref("ns"); + await bRef.set("namespace-b"); + + // Read from a third + const cRef = cApp.database().ref("ns"); + await cRef.once("value"); + + // Ask for export + const exportCLI = new CLIProcess("2", __dirname); + const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); + await exportCLI.start("emulators:export", FIREBASE_PROJECT, [exportPath], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes("Export complete"); + }); + await exportCLI.stop(); + + // Check that the right export files are created + const dbExportPath = path.join(exportPath, "database_export"); + const dbExportFiles = fs.readdirSync(dbExportPath); + expect(dbExportFiles).to.eql(["namespace-a.json", "namespace-b.json"]); + + // Stop the suite + await emulatorsCLI.stop(); + + // Attempt to import + const importCLI = new CLIProcess("3", __dirname); + await importCLI.start( + "emulators:start", + FIREBASE_PROJECT, + ["--only", "database", "--import", exportPath, "--export-on-exit"], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }, + ); + + // Read the data + const aSnap = await aRef.once("value"); + const bSnap = await bRef.once("value"); + expect(aSnap.val()).to.eql("namespace-a"); + expect(bSnap.val()).to.eql("namespace-b"); + + // Delete all of the import files + for (const f of fs.readdirSync(dbExportPath)) { + const fullPath = path.join(dbExportPath, f); + fs.unlinkSync(fullPath); + } + + // Delete all the data in one namespace + await bApp.database().ref().set(null); + + // Stop the CLI (which will export on exit) + await importCLI.stop(); + + // Confirm the data exported is as expected + const aPath = path.join(dbExportPath, "namespace-a.json"); + const aData = JSON.parse(fs.readFileSync(aPath).toString()); + expect(aData).to.deep.equal({ ns: "namespace-a" }); + + const bPath = path.join(dbExportPath, "namespace-b.json"); + const bData = JSON.parse(fs.readFileSync(bPath).toString()); + expect(bData).to.equal(null); + }); + + it("should be able to import/export auth data", async function (this) { + this.timeout(2 * TEST_SETUP_TIMEOUT); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Start up emulator suite + const project = FIREBASE_PROJECT || "example"; + const emulatorsCLI = new CLIProcess("1", __dirname); + + await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }); + + // Create some accounts to export: + const config = readConfig(); + const port = config.emulators!.auth.port; + try { + process.env.FIREBASE_AUTH_EMULATOR_HOST = `${await localhost()}:${port}`; + const adminApp = admin.initializeApp( + { + projectId: project, + credential: ADMIN_CREDENTIAL, + }, + "admin-app", + ); + await adminApp + .auth() + .createUser({ uid: "123", email: "foo@example.com", password: "testing" }); + await adminApp + .auth() + .createUser({ uid: "456", email: "bar@example.com", emailVerified: true }); + + // Ask for export + const exportCLI = new CLIProcess("2", __dirname); + const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); + await exportCLI.start("emulators:export", project, [exportPath], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes("Export complete"); + }); + await exportCLI.stop(); + + // Stop the suite + await emulatorsCLI.stop(); + + // Confirm the data is exported as expected + const configPath = path.join(exportPath, "auth_export", "config.json"); + const configData = JSON.parse(fs.readFileSync(configPath).toString()); + expect(configData).to.deep.equal({ + signIn: { + allowDuplicateEmails: false, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, + }); + + const accountsPath = path.join(exportPath, "auth_export", "accounts.json"); + const accountsData = JSON.parse(fs.readFileSync(accountsPath).toString()); + expect(accountsData.users).to.have.length(2); + expect(accountsData.users[0]).to.deep.contain({ + localId: "123", + email: "foo@example.com", + emailVerified: false, + providerUserInfo: [ + { + email: "foo@example.com", + federatedId: "foo@example.com", + providerId: "password", + rawId: "foo@example.com", + }, + ], + }); + expect(accountsData.users[0].passwordHash).to.match(/:password=testing$/); + expect(accountsData.users[1]).to.deep.contain({ + localId: "456", + email: "bar@example.com", + emailVerified: true, + }); + + // Attempt to import + const importCLI = new CLIProcess("3", __dirname); + await importCLI.start( + "emulators:start", + project, + ["--only", "auth", "--import", exportPath], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }, + ); + + // Check users are indeed imported correctly + const user1 = await adminApp.auth().getUserByEmail("foo@example.com"); + expect(user1.passwordHash).to.match(/:password=testing$/); + const user2 = await adminApp.auth().getUser("456"); + expect(user2.emailVerified).to.be.true; + + await importCLI.stop(); + } finally { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + } + }); + + it("should be able to import/export auth data with many users", async function (this) { + this.timeout(2 * TEST_SETUP_TIMEOUT); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Start up emulator suite + const project = FIREBASE_PROJECT || "example"; + const emulatorsCLI = new CLIProcess("1", __dirname); + + await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }); + + // Create some accounts to export: + const accountCount = 777; // ~120KB data when exported + const config = readConfig(); + const port = config.emulators!.auth.port; + try { + process.env.FIREBASE_AUTH_EMULATOR_HOST = `${await localhost()}:${port}`; + const adminApp = admin.initializeApp( + { + projectId: project, + credential: ADMIN_CREDENTIAL, + }, + "admin-app2", + ); + for (let i = 0; i < accountCount; i++) { + await adminApp + .auth() + .createUser({ uid: `u${i}`, email: `u${i}@example.com`, password: "testing" }); + } + // Ask for export + const exportCLI = new CLIProcess("2", __dirname); + const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); + await exportCLI.start("emulators:export", project, [exportPath], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes("Export complete"); + }); + await exportCLI.stop(); + + // Stop the suite + await emulatorsCLI.stop(); + + // Confirm the data is exported as expected + const configPath = path.join(exportPath, "auth_export", "config.json"); + const configData = JSON.parse(fs.readFileSync(configPath).toString()); + expect(configData).to.deep.equal({ + signIn: { + allowDuplicateEmails: false, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, + }); + + const accountsPath = path.join(exportPath, "auth_export", "accounts.json"); + const accountsData = JSON.parse(fs.readFileSync(accountsPath).toString()); + expect(accountsData.users).to.have.length(accountCount); + + // Attempt to import + const importCLI = new CLIProcess("3", __dirname); + await importCLI.start( + "emulators:start", + project, + ["--only", "auth", "--import", exportPath], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }, + ); + + // Check users are indeed imported correctly + const user = await adminApp.auth().getUserByEmail(`u${accountCount - 1}@example.com`); + expect(user.passwordHash).to.match(/:password=testing$/); + + await importCLI.stop(); + } finally { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + } + }); + it("should be able to export / import auth data with no users", async function (this) { + this.timeout(2 * TEST_SETUP_TIMEOUT); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Start up emulator suite + const project = FIREBASE_PROJECT || "example"; + const emulatorsCLI = new CLIProcess("1", __dirname); + + await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }); + + // Ask for export (with no users) + const exportCLI = new CLIProcess("2", __dirname); + const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); + await exportCLI.start("emulators:export", project, [exportPath], (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes("Export complete"); + }); + await exportCLI.stop(); + + // Stop the suite + await emulatorsCLI.stop(); + + // Confirm the data is exported as expected + const configPath = path.join(exportPath, "auth_export", "config.json"); + const configData = JSON.parse(fs.readFileSync(configPath).toString()); + expect(configData).to.deep.equal({ + signIn: { + allowDuplicateEmails: false, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, + }); + + const accountsPath = path.join(exportPath, "auth_export", "accounts.json"); + const accountsData = JSON.parse(fs.readFileSync(accountsPath).toString()); + expect(accountsData.users).to.have.length(0); + + // Attempt to import + const importCLI = new CLIProcess("3", __dirname); + await importCLI.start( + "emulators:start", + project, + ["--only", "auth", "--import", exportPath], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }, + ); + + await importCLI.stop(); + }); + + it("should be able to import/export storage data", async function (this) { + this.timeout(2 * TEST_SETUP_TIMEOUT); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Start up emulator suite + const emulatorsCLI = new CLIProcess("1", __dirname); + await emulatorsCLI.start( + "emulators:start", + FIREBASE_PROJECT, + ["--only", "storage"], + logIncludes(ALL_EMULATORS_STARTED_LOG), + ); + + const credPath = path.join(__dirname, "service-account-key.json"); + const credential = fs.existsSync(credPath) + ? admin.credential.cert(credPath) + : admin.credential.applicationDefault(); + + const config = readConfig(); + const port = config.emulators!.storage.port; + process.env.STORAGE_EMULATOR_HOST = `http://${await localhost()}:${port}`; + + // Write some data to export + const aApp = admin.initializeApp( + { + projectId: FIREBASE_PROJECT, + storageBucket: "bucket-a", + credential, + }, + "storage-export-a", + ); + const bApp = admin.initializeApp( + { + projectId: FIREBASE_PROJECT, + storageBucket: "bucket-b", + credential, + }, + "storage-export-b", + ); + + // Write data to two buckets + await aApp.storage().bucket().file("a/b.txt").save("a/b hello, world!"); + await aApp.storage().bucket().file("c/d.txt").save("c/d hello, world!"); + await bApp.storage().bucket().file("e/f.txt").save("e/f hello, world!"); + await bApp.storage().bucket().file("g/h.txt").save("g/h hello, world!"); + + // Ask for export + const exportCLI = new CLIProcess("2", __dirname); + const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); + await exportCLI.start( + "emulators:export", + FIREBASE_PROJECT, + [exportPath], + logIncludes("Export complete"), + ); + await exportCLI.stop(); + + // Check that the right export files are created + const storageExportPath = path.join(exportPath, "storage_export"); + const storageExportFiles = fs.readdirSync(storageExportPath).sort(); + expect(storageExportFiles).to.eql(["blobs", "buckets.json", "metadata"]); + + // Stop the suite + await emulatorsCLI.stop(); + + // Attempt to import + const importCLI = new CLIProcess("3", __dirname); + await importCLI.start( + "emulators:start", + FIREBASE_PROJECT, + ["--only", "storage", "--import", exportPath], + logIncludes(ALL_EMULATORS_STARTED_LOG), + ); + + // List the files + const [aFiles] = await aApp.storage().bucket().getFiles({ + prefix: "a/", + }); + const aFileNames = aFiles.map((f) => f.name).sort(); + expect(aFileNames).to.eql(["a/b.txt"]); + + const [bFiles] = await bApp.storage().bucket().getFiles({ + prefix: "e/", + }); + const bFileNames = bFiles.map((f) => f.name).sort(); + expect(bFileNames).to.eql(["e/f.txt"]); + + // TODO: this operation fails due to a bug in the Storage emulator + // https://github.com/firebase/firebase-tools/pull/3320 + // + // Read a file and check content + // const [f] = await aApp.storage().bucket().file("a/b.txt").get(); + // const [buf] = await f.download(); + // expect(buf.toString()).to.eql("a/b hello, world!"); + }); +}); diff --git a/scripts/emulator-tests/.gitignore b/scripts/emulator-tests/.gitignore new file mode 100644 index 00000000000..1fcac224405 --- /dev/null +++ b/scripts/emulator-tests/.gitignore @@ -0,0 +1 @@ +./functions/index.js diff --git a/scripts/emulator-tests/fixtures.ts b/scripts/emulator-tests/fixtures.ts index 2b991af92c5..8beb968d263 100644 --- a/scripts/emulator-tests/fixtures.ts +++ b/scripts/emulator-tests/fixtures.ts @@ -6,17 +6,6 @@ export const TIMEOUT_MED = 5000; export const MODULE_ROOT = findModuleRoot("firebase-tools", __dirname); export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = { onCreate: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { value: { @@ -41,22 +30,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "us-central1-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onWrite: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { value: { @@ -81,22 +56,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "us-central1-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onDelete: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { oldValue: { @@ -121,22 +82,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "us-central1-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onUpdate: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { oldValue: { @@ -173,25 +120,9 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = timestamp: "2019-05-15T16:21:15.148831Z", }, }, - triggerId: "us-central1-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onRequest: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, - triggerId: "us-central1-function_id", - targetName: "function_id", - projectId: "fake-project-id", + proto: {}, }, }; diff --git a/scripts/emulator-tests/functions/package-lock.json b/scripts/emulator-tests/functions/package-lock.json new file mode 100644 index 00000000000..b8a74e48c48 --- /dev/null +++ b/scripts/emulator-tests/functions/package-lock.json @@ -0,0 +1,4965 @@ +{ + "name": "test-fns", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "test-fns", + "version": "0.0.1", + "dependencies": { + "express": "^4.18.1", + "firebase-admin": "^11.5.0", + "firebase-functions": "^4.0.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "optional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/component": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.1.tgz", + "integrity": "sha512-yvKthG0InjFx9aOPnh6gk0lVNfNVEtyq3LwXgZr+hOwD0x/CtXq33XCpqv0sQj5CA4FdMy8OO+y9edI+ZUw8LA==", + "dependencies": { + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.1.tgz", + "integrity": "sha512-iX6/p7hoxUMbYAGZD+D97L05xQgpkslF2+uJLZl46EdaEfjVMEwAdy7RS/grF96kcFZFg502LwPYTXoIdrZqOA==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.1.tgz", + "integrity": "sha512-sI7LNh0C8PCq9uUKjrBKLbZvqHTSjsf2LeZRxin+rHVegomjsOAYk9OzYwxETWh3URhpMkCM8KcTl7RVwAldog==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/database": "0.14.1", + "@firebase/database-types": "0.10.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.1.tgz", + "integrity": "sha512-UgUx9VakTHbP2WrVUdYrUT2ofTFVfWjGW2O1fwuvvMyo6WSnuSyO5nB1u0cyoMPvO25dfMIUVerfK7qFfwGL3Q==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.0" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.0.tgz", + "integrity": "sha512-oeoq/6Sr9btbwUQs5HPfeww97bf7qgBbkknbDTXpRaph2LZ23O9XLCE5tJy856SBmGQfO4xBZP8dyryLLM2nSQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "optional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.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==", + "optional": true + }, + "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==", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "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==", + "optional": true, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.32", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz", + "integrity": "sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", + "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "optional": true + }, + "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==", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "optional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "node_modules/@types/node": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.4.tgz", + "integrity": "sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "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==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "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==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "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==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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" + } + ], + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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==", + "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==", + "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==", + "optional": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "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==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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==" + }, + "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==", + "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==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "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==" + }, + "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==", + "optional": true + }, + "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==", + "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==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "optional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "optional": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "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==", + "optional": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true + }, + "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==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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": "11.5.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.5.0.tgz", + "integrity": "sha512-bBdlYtNvXx8yZGdCd00NrfZl1o1A0aXOw5h8q5PwC8RXikOLNXq8vYtSKW44dj8zIaafVP6jFdcUXZem/LMsHA==", + "dependencies": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.3.0", + "@firebase/database-types": "^0.10.0", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2" + } + }, + "node_modules/firebase-functions": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.2.0.tgz", + "integrity": "sha512-WvC+yeqez769dcgJ8YqGYOHRsB+tzVN6CYV7AARmulhKUOvIP+EqUXK5LQFR1nB01/2LGpeK39uBXh42CPSTpg==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==" + }, + "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==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "node_modules/jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "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==", + "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==", + "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==" + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "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.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", + "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", + "dependencies": { + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^9.0.0", + "debug": "^4.3.4", + "jose": "^4.10.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz", + "integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "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==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "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==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "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==", + "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==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "optional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "optional": true, + "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/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "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==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "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==", + "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==", + "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==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "optional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "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==", + "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==" + }, + "node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "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==", + "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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/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==", + "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==" + }, + "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==", + "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==", + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true, + "engines": { + "node": ">=0.10.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==", + "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==", + "optional": true + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "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==", + "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==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "optional": true + }, + "@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "requires": { + "text-decoding": "^1.0.0" + } + }, + "@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "@firebase/component": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.1.tgz", + "integrity": "sha512-yvKthG0InjFx9aOPnh6gk0lVNfNVEtyq3LwXgZr+hOwD0x/CtXq33XCpqv0sQj5CA4FdMy8OO+y9edI+ZUw8LA==", + "requires": { + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.1.tgz", + "integrity": "sha512-iX6/p7hoxUMbYAGZD+D97L05xQgpkslF2+uJLZl46EdaEfjVMEwAdy7RS/grF96kcFZFg502LwPYTXoIdrZqOA==", + "requires": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.1.tgz", + "integrity": "sha512-sI7LNh0C8PCq9uUKjrBKLbZvqHTSjsf2LeZRxin+rHVegomjsOAYk9OzYwxETWh3URhpMkCM8KcTl7RVwAldog==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/database": "0.14.1", + "@firebase/database-types": "0.10.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.1.tgz", + "integrity": "sha512-UgUx9VakTHbP2WrVUdYrUT2ofTFVfWjGW2O1fwuvvMyo6WSnuSyO5nB1u0cyoMPvO25dfMIUVerfK7qFfwGL3Q==", + "requires": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.0" + } + }, + "@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.0.tgz", + "integrity": "sha512-oeoq/6Sr9btbwUQs5HPfeww97bf7qgBbkknbDTXpRaph2LZ23O9XLCE5tJy856SBmGQfO4xBZP8dyryLLM2nSQ==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + } + }, + "@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true + }, + "@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true + }, + "@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "requires": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "dependencies": { + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true + } + } + }, + "@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + } + }, + "@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "@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==", + "optional": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "optional": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "optional": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "optional": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "optional": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "optional": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "optional": true + }, + "@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==", + "optional": true + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.32", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz", + "integrity": "sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "requires": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "@types/jsonwebtoken": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", + "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "requires": { + "@types/node": "*" + } + }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "optional": true + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "optional": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "optional": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "@types/node": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.4.tgz", + "integrity": "sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "optional": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "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==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "optional": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "requires": {} + }, + "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==", + "optional": true, + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "optional": true + }, + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "optional": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "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==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "optional": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "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==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "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==", + "optional": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "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==", + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "optional": true + }, + "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==" + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "optional": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "optional": true + }, + "espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "optional": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "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==", + "optional": true + }, + "express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "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==", + "optional": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "firebase-admin": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.5.0.tgz", + "integrity": "sha512-bBdlYtNvXx8yZGdCd00NrfZl1o1A0aXOw5h8q5PwC8RXikOLNXq8vYtSKW44dj8zIaafVP6jFdcUXZem/LMsHA==", + "requires": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.3.0", + "@firebase/database-types": "^0.10.0", + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + } + }, + "firebase-functions": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.2.0.tgz", + "integrity": "sha512-WvC+yeqez769dcgJ8YqGYOHRsB+tzVN6CYV7AARmulhKUOvIP+EqUXK5LQFR1nB01/2LGpeK39uBXh42CPSTpg==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "optional": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "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==", + "optional": true + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "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==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "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==" + }, + "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==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "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==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==" + }, + "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==", + "optional": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==" + }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "requires": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + } + }, + "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==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", + "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", + "requires": { + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^9.0.0", + "debug": "^4.3.4", + "jose": "^4.10.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "dependencies": { + "@types/express": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.15.tgz", + "integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "optional": true + }, + "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==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "requires": {} + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "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==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "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==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true + }, + "proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "optional": true, + "requires": { + "protobufjs": "^7.0.0" + } + }, + "protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "optional": true, + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + } + } + }, + "protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "requires": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + } + }, + "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==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "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==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "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==", + "optional": true + }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true + }, + "retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "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==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "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" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "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==", + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "optional": true + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + } + }, + "text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "optional": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "optional": true + }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true + }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "optional": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "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==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "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==" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "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==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "optional": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "requires": { + "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" + } + }, + "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==", + "optional": true + }, + "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==", + "optional": true + } + } +} diff --git a/scripts/emulator-tests/functions/package.json b/scripts/emulator-tests/functions/package.json new file mode 100644 index 00000000000..97967bd5a20 --- /dev/null +++ b/scripts/emulator-tests/functions/package.json @@ -0,0 +1,14 @@ +{ + "name": "test-fns", + "version": "0.0.1", + "description": "Test function package for functions emulator integration tests", + "main": "index.js", + "dependencies": { + "express": "^4.18.1", + "firebase-admin": "^11.5.0", + "firebase-functions": "^4.0.0" + }, + "engines": { + "node": "20" + } +} diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index 026411cdad6..fc1e04ecd77 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -1,17 +1,22 @@ +import * as fs from "fs"; +import * as fsp from "fs/promises"; +import * as path from "path"; + import { expect } from "chai"; import * as express from "express"; import * as sinon from "sinon"; import * as supertest from "supertest"; +import * as winston from "winston"; +import * as logform from "logform"; -import { SignatureType } from "../../src/emulator/functionsEmulatorShared"; -import { FunctionsEmulator, InvokeRuntimeOpts } from "../../src/emulator/functionsEmulator"; -import { Emulators } from "../../src/emulator/types"; -import { RuntimeWorker } from "../../src/emulator/functionsRuntimeWorker"; +import { EmulatedTriggerDefinition } from "../../src/emulator/functionsEmulatorShared"; +import { EmulatableBackend, FunctionsEmulator } from "../../src/emulator/functionsEmulator"; +import { EmulatorInfo, Emulators } from "../../src/emulator/types"; +import { FakeEmulator } from "../../src/emulator/testing/fakeEmulator"; import { TIMEOUT_LONG, TIMEOUT_MED, MODULE_ROOT } from "./fixtures"; import { logger } from "../../src/logger"; import * as registry from "../../src/emulator/registry"; -import * as winston from "winston"; -import * as logform from "logform"; +import * as secretManager from "../../src/gcp/secretManager"; if ((process.env.DEBUG || "").toLowerCase().includes("spec")) { const dropLogLevels = (info: logform.TransformableInfo) => info.message; @@ -20,668 +25,1046 @@ if ((process.env.DEBUG || "").toLowerCase().includes("spec")) { level: "debug", format: logform.format.combine( logform.format.colorize(), - logform.format.printf(dropLogLevels) + logform.format.printf(dropLogLevels), ), - }) + }), ); } -const functionsEmulator = new FunctionsEmulator({ - projectId: "fake-project-id", - functionsDir: MODULE_ROOT, - quiet: true, -}); +const FUNCTIONS_DIR = path.resolve( + // MODULE_ROOT points to firebase-tools/dev since that's where this test file is compiled to. + // Function source directory is located on firebase-tools/ hence the "..". See run.sh + path.join(MODULE_ROOT, "..", "scripts/emulator-tests/functions"), + // path.join(MODULE_ROOT, "scripts/emulator-tests/functions") +); -// This is normally discovered in FunctionsEmulator#start() -functionsEmulator.nodeBinary = process.execPath; - -functionsEmulator.setTriggersForTesting([ - { - platform: "gcfv1", - name: "function_id", - id: "us-central1-function_id", - region: "us-central1", - entryPoint: "function_id", - httpsTrigger: {}, - labels: {}, - }, - { - platform: "gcfv1", - name: "function_id", - id: "europe-west2-function_id", - region: "europe-west2", - entryPoint: "function_id", - httpsTrigger: {}, - labels: {}, - }, - { - platform: "gcfv1", - name: "function_id", - id: "europe-west3-function_id", - region: "europe-west3", - entryPoint: "function_id", - httpsTrigger: {}, - labels: {}, - }, - { - platform: "gcfv1", - name: "callable_function_id", - id: "us-central1-callable_function_id", - region: "us-central1", - entryPoint: "callable_function_id", - httpsTrigger: {}, - labels: { - "deployment-callable": "true", - }, - }, - { - platform: "gcfv1", - name: "nested-function_id", - id: "us-central1-nested-function_id", - region: "us-central1", - entryPoint: "nested.function_id", - httpsTrigger: {}, - labels: {}, - }, -]); - -// TODO(samstern): This is an ugly way to just override the InvokeRuntimeOpts on each call -const startFunctionRuntime = functionsEmulator.startFunctionRuntime.bind(functionsEmulator); -function useFunctions(triggers: () => {}): void { - const serializedTriggers = triggers.toString(); - - // eslint-disable-next-line @typescript-eslint/unbound-method - functionsEmulator.startFunctionRuntime = ( - triggerId: string, - targetName: string, - triggerType: SignatureType, - proto?: any, - runtimeOpts?: InvokeRuntimeOpts - ): RuntimeWorker => { - return startFunctionRuntime(triggerId, targetName, triggerType, proto, { - nodeBinary: process.execPath, - serializedTriggers, - }); +const TEST_BACKEND: EmulatableBackend = { + functionsDir: FUNCTIONS_DIR, + env: {}, + secretEnv: [], + codebase: "default", + runtime: "nodejs14", + bin: process.execPath, + // NOTE: Use the following node bin path if you want to run test cases directly from your IDE. + // bin: path.join(MODULE_ROOT, "node_modules/.bin/ts-node"), +}; + +async function setupEnvFiles(envs: Record) { + const envFiles: string[] = []; + for (const [filename, data] of Object.entries(envs)) { + const envPath = path.join(FUNCTIONS_DIR, filename); + await fsp.writeFile(path.join(FUNCTIONS_DIR, filename), data); + envFiles.push(envPath); + } + return async () => { + await Promise.all(envFiles.map((f) => fsp.rm(f))); }; } -describe("FunctionsEmulator-Hub", () => { - it("should route requests to /:project_id/us-central1/:trigger_id to default region HTTPS Function", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - } - ), - }; +async function writeSource( + triggerSource: () => void, + params?: Record void>, +): Promise<() => Promise> { + let sourceCode = `module.exports = (${triggerSource.toString()})();\n`; + const sourcePath = path.join(FUNCTIONS_DIR, "index.js"); + if (params) { + for (const [paramName, valFn] of Object.entries(params)) { + sourceCode = `const ${paramName} = (${valFn.toString()})();\n${sourceCode}`; + // Since parameter cannot be references before it's defined, employ this hack to + // replace all "string-escaped" param references to real instances. + sourceCode = sourceCode.replaceAll(`"__$${paramName}__"`, paramName); + } + } + await fsp.writeFile(sourcePath, sourceCode); + return async () => { + await fsp.rm(sourcePath); + }; +} + +async function useFunction( + emu: FunctionsEmulator, + triggerName: string, + triggerSource: () => {}, + regions: string[] = ["us-central1"], + triggerOverrides?: Partial, +): Promise { + await writeSource(triggerSource); + const triggers: EmulatedTriggerDefinition[] = []; + for (const region of regions) { + triggers.push({ + platform: "gcfv1", + name: triggerName, + entryPoint: triggerName.replace(/-/g, "."), + id: `${region}-${triggerName}`, + region, + codebase: "default", + httpsTrigger: {}, + ...triggerOverrides, }); + } + emu.setTriggersForTesting(triggers, TEST_BACKEND); +} - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id") - .expect(200) - .then((res) => { - expect(res.body.path).to.deep.equal("/"); - }); - }).timeout(TIMEOUT_LONG); - - it("should route requests to /:project_id/:other-region/:trigger_id to the region's HTTPS Function", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .region("us-central1", "europe-west2") - .https.onRequest((req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - }), - }; +const TEST_PROJECT_ID = "fake-project-id"; + +describe("FunctionsEmulator", function () { + // eslint-disable-next-line @typescript-eslint/no-invalid-this + this.timeout(TIMEOUT_LONG); + + let emu: FunctionsEmulator; + + beforeEach(() => { + emu = new FunctionsEmulator({ + projectId: TEST_PROJECT_ID, + projectDir: MODULE_ROOT, + emulatableBackends: [TEST_BACKEND], + verbosity: "QUIET", + debugPort: false, + adminSdkConfig: { + projectId: TEST_PROJECT_ID, + databaseURL: `https://${TEST_PROJECT_ID}-default-rtdb.firebaseio.com`, + storageBucket: `${TEST_PROJECT_ID}.appspot.com`, + }, }); + }); + + afterEach(async () => { + await emu.stop(); + }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/europe-west2/function_id") - .expect(200) - .then((res) => { - expect(res.body.path).to.deep.equal("/"); + describe("Hub", () => { + it("should route requests to /:project_id/us-central1/:trigger_id to default region HTTPS Function", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should 404 when a function doesn't exist in the region", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .region("us-central1", "europe-west2") - .https.onRequest((req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - }), - }; + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId`) + .expect(200) + .then((res) => { + expect(res.body.path).to.deep.equal("/"); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-east1/function_id") - .expect(404); - }).timeout(TIMEOUT_LONG); - - it("should route requests to /:project_id/:region/:trigger_id/ to HTTPS Function", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - } - ), - }; + it("should route requests to /:project_id/:other-region/:trigger_id to the region's HTTPS Function", async () => { + await useFunction( + emu, + "functionId", + () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .region("us-central1", "europe-west2") + .https.onRequest((req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }), + }; + }, + ["us-central1", "europe-west2"], + ); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/europe-west2/functionId`) + .expect(200) + .then((res) => { + expect(res.body.path).to.deep.equal("/"); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id/") - .expect(200) - .then((res) => { - expect(res.body.path).to.deep.equal("/"); - }); - }).timeout(TIMEOUT_LONG); - - it("should 404 when a function does not exist", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - } - ), - }; + it("should 404 when a function doesn't exist in the region", async () => { + await useFunction( + emu, + "functionId", + () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .region("us-central1", "europe-west2") + .https.onRequest((req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }), + }; + }, + ["us-central1", "europe-west2"], + ); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-east1/functionId`) + .expect(404); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_dne") - .expect(404); - }).timeout(TIMEOUT_LONG); - - it("should properly route to a namespaced/grouped HTTPs function", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - nested: { - function_id: require("firebase-functions").https.onRequest( + it("should route requests to /:project_id/:region/:trigger_id/ to HTTPS Function", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( (req: express.Request, res: express.Response) => { res.json({ path: req.path }); - } + }, ), - }, - }; + }; + }); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId/`) + .expect(200) + .then((res) => { + expect(res.body.path).to.deep.equal("/"); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/nested-function_id") - .expect(200) - .then((res) => { - expect(res.body.path).to.deep.equal("/"); + it("should 404 when a function does not exist", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should route requests to /:project_id/:region/:trigger_id/a/b to HTTPS Function", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - } - ), - }; + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionDNE`) + .expect(404); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id/a/b") - .expect(200) - .then((res) => { - expect(res.body.path).to.deep.equal("/a/b"); + it("should properly route to a namespaced/grouped HTTPs function", async () => { + await useFunction(emu, "nested-functionId", () => { + return { + nested: { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }, + }; }); - }).timeout(TIMEOUT_LONG); - - it("should reject requests to a non-emulator path", async () => { - useFunctions(() => { - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - } - ), - }; + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/nested-functionId`) + .expect(200) + .then((res) => { + expect(res.body.path).to.deep.equal("/"); + }); }); - await supertest(functionsEmulator.createHubServer()).get("/foo/bar/baz").expect(404); - }).timeout(TIMEOUT_LONG); - - it("should rewrite req.path to hide /:project_id/:region/:trigger_id", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ path: req.path }); - } - ), - }; + it("should route requests to /:project_id/:region/:trigger_id/a/b to HTTPS Function", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; + }); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId/a/b`) + .expect(200) + .then((res) => { + expect(res.body.path).to.deep.equal("/a/b"); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id/sub/route/a") - .expect(200) - .then((res) => { - expect(res.body.path).to.eq("/sub/route/a"); + it("should reject requests to a non-emulator path", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should return the correct url, baseUrl, originalUrl for the root route", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ - url: req.url, - baseUrl: req.baseUrl, - originalUrl: req.originalUrl, - }); - } - ), - }; + + await supertest(emu.createHubServer()).get("/foo/bar/baz").expect(404); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id") - .expect(200) - .then((res) => { - expect(res.body.url).to.eq("/"); - expect(res.body.baseUrl).to.eq(""); - expect(res.body.originalUrl).to.eq("/"); + it("should rewrite req.path to hide /:project_id/:region/:trigger_id", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should return the correct url, baseUrl, originalUrl with query params", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ - url: req.url, - baseUrl: req.baseUrl, - originalUrl: req.originalUrl, - query: req.query, - }); - } - ), - }; + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId/sub/route/a`) + .expect(200) + .then((res) => { + expect(res.body.path).to.eq("/sub/route/a"); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id?a=1&b=2") - .expect(200) - .then((res) => { - expect(res.body.url).to.eq("/?a=1&b=2"); - expect(res.body.baseUrl).to.eq(""); - expect(res.body.originalUrl).to.eq("/?a=1&b=2"); - expect(res.body.query).to.deep.eq({ a: "1", b: "2" }); + it("should return the correct url, baseUrl, originalUrl for the root route", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + url: req.url, + baseUrl: req.baseUrl, + originalUrl: req.originalUrl, + }); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should return the correct url, baseUrl, originalUrl for a subroute", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json({ - url: req.url, - baseUrl: req.baseUrl, - originalUrl: req.originalUrl, - }); - } - ), - }; + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId`) + .expect(200) + .then((res) => { + expect(res.body.url).to.eq("/"); + expect(res.body.baseUrl).to.eq(""); + expect(res.body.originalUrl).to.eq("/"); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id/sub/route/a") - .expect(200) - .then((res) => { - expect(res.body.url).to.eq("/sub/route/a"); - expect(res.body.baseUrl).to.eq(""); - expect(res.body.originalUrl).to.eq("/sub/route/a"); + it("should return the correct url, baseUrl, originalUrl with query params", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + url: req.url, + baseUrl: req.baseUrl, + originalUrl: req.originalUrl, + query: req.query, + }); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should return the correct url, baseUrl, originalUrl for any region", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .region("europe-west3") - .https.onRequest((req: express.Request, res: express.Response) => { - res.json({ - url: req.url, - baseUrl: req.baseUrl, - originalUrl: req.originalUrl, - }); - }), - }; + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId?a=1&b=2`) + .expect(200) + .then((res) => { + expect(res.body.url).to.eq("/?a=1&b=2"); + expect(res.body.baseUrl).to.eq(""); + expect(res.body.originalUrl).to.eq("/?a=1&b=2"); + expect(res.body.query).to.deep.eq({ a: "1", b: "2" }); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/europe-west3/function_id") - .expect(200) - .then((res) => { - expect(res.body.url).to.eq("/"); - expect(res.body.baseUrl).to.eq(""); - expect(res.body.originalUrl).to.eq("/"); + it("should return the correct url, baseUrl, originalUrl for a subroute", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + url: req.url, + baseUrl: req.baseUrl, + originalUrl: req.originalUrl, + }); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should route request body", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json(req.body); - } - ), - }; + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId/sub/route/a`) + .expect(200) + .then((res) => { + expect(res.body.url).to.eq("/sub/route/a"); + expect(res.body.baseUrl).to.eq(""); + expect(res.body.originalUrl).to.eq("/sub/route/a"); + }); + }); + + it("should return the correct url, baseUrl, originalUrl for any region", async () => { + await useFunction( + emu, + "functionId", + () => { + return { + functionId: require("firebase-functions") + .region("europe-west3") + .https.onRequest((req: express.Request, res: express.Response) => { + res.json({ + url: req.url, + baseUrl: req.baseUrl, + originalUrl: req.originalUrl, + query: req.query, + }); + }), + }; + }, + ["europe-west3"], + ); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/europe-west3/functionId?a=1&b=2`) + .expect(200) + .then((res) => { + expect(res.body.url).to.eq("/?a=1&b=2"); + expect(res.body.baseUrl).to.eq(""); + expect(res.body.originalUrl).to.eq("/?a=1&b=2"); + expect(res.body.query).to.deep.eq({ a: "1", b: "2" }); + }); }); - await supertest(functionsEmulator.createHubServer()) - .post("/fake-project-id/us-central1/function_id/sub/route/a") - .send({ hello: "world" }) - .expect(200) - .then((res) => { - expect(res.body).to.deep.equal({ hello: "world" }); + it("should route request body", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json(req.body); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - - it("should route query parameters", async () => { - useFunctions(() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - (req: express.Request, res: express.Response) => { - res.json(req.query); - } - ), - }; + + await supertest(emu.createHubServer()) + .post(`/${TEST_PROJECT_ID}/us-central1/functionId/sub/route/a`) + .send({ hello: "world" }) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ hello: "world" }); + }); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id/sub/route/a?hello=world") - .expect(200) - .then((res) => { - expect(res.body).to.deep.equal({ hello: "world" }); + it("should route query parameters", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json(req.query); + }, + ), + }; }); - }).timeout(TIMEOUT_LONG); - it("should override callable auth", async () => { - useFunctions(() => { - return { - callable_function_id: require("firebase-functions").https.onCall((data: any, ctx: any) => { - return { - auth: ctx.auth, - }; - }), - }; + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId/sub/route/a?hello=world`) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ hello: "world" }); + }); }); - // For token info: - // https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw - await supertest(functionsEmulator.createHubServer()) - .post("/fake-project-id/us-central1/callable_function_id") - .set({ - "Content-Type": "application/json", - Authorization: - "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw", - }) - .send({ data: {} }) - .expect(200) - .then((res) => { - expect(res.body).to.deep.equal({ - result: { - auth: { - uid: "Smnz8O1rldfjmdx8ARUtMvXmmw62", - token: { - provider_id: "anonymous", - iss: "https://securetoken.google.com/fir-dumpster", - aud: "fir-dumpster", - auth_time: 1585053264, - user_id: "Smnz8O1rldfjmdx8ARUtMvXmmw62", - sub: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + it("should override callable auth", async () => { + await useFunction(emu, "callableFunctionId", () => { + return { + callableFunctionId: require("firebase-functions").https.onCall((data: any, ctx: any) => { + return { + auth: ctx.auth, + }; + }), + }; + }); + + // For token info: + // https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw + await supertest(emu.createHubServer()) + .post(`/${TEST_PROJECT_ID}/us-central1/callableFunctionId`) + .set({ + "Content-Type": "application/json", + Authorization: + "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw", + }) + .send({ data: {} }) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ + result: { + auth: { uid: "Smnz8O1rldfjmdx8ARUtMvXmmw62", - iat: 1585053264, - exp: 1585056864, - firebase: { - identities: {}, - sign_in_provider: "anonymous", + token: { + provider_id: "anonymous", + iss: "https://securetoken.google.com/fir-dumpster", + aud: "fir-dumpster", + auth_time: 1585053264, + user_id: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + sub: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + uid: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + iat: 1585053264, + exp: 1585056864, + firebase: { + identities: {}, + sign_in_provider: "anonymous", + }, }, }, }, - }, + }); }); + }); + + it("should override callable auth with unicode", async () => { + await useFunction(emu, "callableFunctionId", () => { + return { + callableFunctionId: require("firebase-functions").https.onCall((data: any, ctx: any) => { + return { + auth: ctx.auth, + }; + }), + }; }); - }).timeout(TIMEOUT_LONG); - it("should override callable auth with unicode", async () => { - useFunctions(() => { - return { - callable_function_id: require("firebase-functions").https.onCall((data: any, ctx: any) => { - return { - auth: ctx.auth, - }; - }), - }; + // For token info: + // https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsIm5hbWUiOiLlsbHnlLDlpKrpg44iLCJ1c2VyX2lkIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsInN1YiI6IlNtbno4TzFybGRmam1keDhBUlV0TXZYbW13NjIiLCJpYXQiOjE1ODUwNTMyNjQsImV4cCI6MTU4NTA1Njg2NCwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6e30sInNpZ25faW5fcHJvdmlkZXIiOiJhbm9ueW1vdXMifX0.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw + await supertest(emu.createHubServer()) + .post(`/${TEST_PROJECT_ID}/us-central1/callableFunctionId`) + .set({ + "Content-Type": "application/json", + Authorization: + "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsIm5hbWUiOiLlsbHnlLDlpKrpg44iLCJ1c2VyX2lkIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsInN1YiI6IlNtbno4TzFybGRmam1keDhBUlV0TXZYbW13NjIiLCJpYXQiOjE1ODUwNTMyNjQsImV4cCI6MTU4NTA1Njg2NCwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6e30sInNpZ25faW5fcHJvdmlkZXIiOiJhbm9ueW1vdXMifX0.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw", + }) + .send({ data: {} }) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ + result: { + auth: { + uid: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + token: { + provider_id: "anonymous", + iss: "https://securetoken.google.com/fir-dumpster", + aud: "fir-dumpster", + auth_time: 1585053264, + name: "山田太郎", + user_id: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + sub: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + uid: "Smnz8O1rldfjmdx8ARUtMvXmmw62", + iat: 1585053264, + exp: 1585056864, + firebase: { + identities: {}, + sign_in_provider: "anonymous", + }, + }, + }, + }, + }); + }); }); - // For token info: - // https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsIm5hbWUiOiLlsbHnlLDlpKrpg44iLCJ1c2VyX2lkIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsInN1YiI6IlNtbno4TzFybGRmam1keDhBUlV0TXZYbW13NjIiLCJpYXQiOjE1ODUwNTMyNjQsImV4cCI6MTU4NTA1Njg2NCwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6e30sInNpZ25faW5fcHJvdmlkZXIiOiJhbm9ueW1vdXMifX0.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw - await supertest(functionsEmulator.createHubServer()) - .post("/fake-project-id/us-central1/callable_function_id") - .set({ - "Content-Type": "application/json", - Authorization: - "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsIm5hbWUiOiLlsbHnlLDlpKrpg44iLCJ1c2VyX2lkIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsInN1YiI6IlNtbno4TzFybGRmam1keDhBUlV0TXZYbW13NjIiLCJpYXQiOjE1ODUwNTMyNjQsImV4cCI6MTU4NTA1Njg2NCwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6e30sInNpZ25faW5fcHJvdmlkZXIiOiJhbm9ueW1vdXMifX0.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw", - }) - .send({ data: {} }) - .expect(200) - .then((res) => { - expect(res.body).to.deep.equal({ - result: { - auth: { - uid: "Smnz8O1rldfjmdx8ARUtMvXmmw62", - token: { - provider_id: "anonymous", - iss: "https://securetoken.google.com/fir-dumpster", - aud: "fir-dumpster", - auth_time: 1585053264, - name: "山田太郎", - user_id: "Smnz8O1rldfjmdx8ARUtMvXmmw62", - sub: "Smnz8O1rldfjmdx8ARUtMvXmmw62", - uid: "Smnz8O1rldfjmdx8ARUtMvXmmw62", - iat: 1585053264, - exp: 1585056864, - firebase: { - identities: {}, - sign_in_provider: "anonymous", + it("should override callable auth with a poorly padded ID Token", async () => { + await useFunction(emu, "callableFunctionId", () => { + return { + callableFunctionId: require("firebase-functions").https.onCall((data: any, ctx: any) => { + return { + auth: ctx.auth, + }; + }), + }; + }); + + // For token info: + // https://jwt.io/#debugger-io?token=eyJhbGciOiJub25lIiwia2lkIjoiZmFrZWtpZCJ9.eyJ1aWQiOiJhbGljZSIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJpYXQiOjAsInN1YiI6ImFsaWNlIn0%3D. + await supertest(emu.createHubServer()) + .post(`/${TEST_PROJECT_ID}/us-central1/callableFunctionId`) + .set({ + "Content-Type": "application/json", + Authorization: + "Bearer eyJhbGciOiJub25lIiwia2lkIjoiZmFrZWtpZCJ9.eyJ1aWQiOiJhbGljZSIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJpYXQiOjAsInN1YiI6ImFsaWNlIn0=.", + }) + .send({ data: {} }) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ + result: { + auth: { + uid: "alice", + token: { + uid: "alice", + email: "alice@example.com", + iat: 0, + sub: "alice", }, }, }, - }, + }); + }); + }); + + it("should preserve the Authorization header for callable auth", async () => { + await useFunction(emu, "callableFunctionId", () => { + return { + callableFunctionId: require("firebase-functions").https.onCall((data: any, ctx: any) => { + return { + header: ctx.rawRequest.headers["authorization"], + }; + }), + }; + }); + + const authHeader = + "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw"; + // For token info: + // https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw + await supertest(emu.createHubServer()) + .post(`/${TEST_PROJECT_ID}/us-central1/callableFunctionId`) + .set({ + "Content-Type": "application/json", + Authorization: authHeader, + }) + .send({ data: {} }) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ + result: { + header: authHeader, + }, + }); }); + }); + + it("should respond to requests to /backends to with info about the running backends", async () => { + await useFunction(emu, "functionId", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; + }); + + await supertest(emu.createHubServer()) + .get("/backends") + .expect(200) + .then((res) => { + // TODO(b/216642962): Add tests for this endpoint that validate behavior when there are Extensions running + expect(res.body.backends.length).to.equal(1); + expect(res.body.backends[0].functionTriggers).to.deep.equal([ + { + entryPoint: "functionId", + httpsTrigger: {}, + id: "us-central1-functionId", + name: "functionId", + platform: "gcfv1", + codebase: "default", + region: "us-central1", + }, + ]); + }); + }); + + describe("system environment variables", () => { + const startFakeEmulator = async (emulator: Emulators): Promise => { + const fake = await FakeEmulator.create(emulator); + await registry.EmulatorRegistry.start(fake); + return fake.getInfo(); + }; + + afterEach(() => { + return registry.EmulatorRegistry.stopAll(); }); - }).timeout(TIMEOUT_LONG); - it("should override callable auth with a poorly padded ID Token", async () => { - useFunctions(() => { - return { - callable_function_id: require("firebase-functions").https.onCall((data: any, ctx: any) => { + it("should set env vars when the emulator is running", async () => { + const database = await startFakeEmulator(Emulators.DATABASE); + const firestore = await startFakeEmulator(Emulators.FIRESTORE); + const auth = await startFakeEmulator(Emulators.AUTH); + + await useFunction(emu, "functionId", () => { return { - auth: ctx.auth, + functionId: require("firebase-functions").https.onRequest( + (_req: express.Request, res: express.Response) => { + res.json({ + databaseHost: process.env.FIREBASE_DATABASE_EMULATOR_HOST, + firestoreHost: process.env.FIRESTORE_EMULATOR_HOST, + authHost: process.env.FIREBASE_AUTH_EMULATOR_HOST, + }); + }, + ), }; - }), - }; - }); + }); - // For token info: - // https://jwt.io/#debugger-io?token=eyJhbGciOiJub25lIiwia2lkIjoiZmFrZWtpZCJ9.eyJ1aWQiOiJhbGljZSIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJpYXQiOjAsInN1YiI6ImFsaWNlIn0%3D. - await supertest(functionsEmulator.createHubServer()) - .post("/fake-project-id/us-central1/callable_function_id") - .set({ - "Content-Type": "application/json", - Authorization: - "Bearer eyJhbGciOiJub25lIiwia2lkIjoiZmFrZWtpZCJ9.eyJ1aWQiOiJhbGljZSIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJpYXQiOjAsInN1YiI6ImFsaWNlIn0=.", - }) - .send({ data: {} }) - .expect(200) - .then((res) => { - expect(res.body).to.deep.equal({ - result: { - auth: { - uid: "alice", - token: { - uid: "alice", - email: "alice@example.com", - iat: 0, - sub: "alice", + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId`) + .expect(200) + .then((res) => { + expect(res.body.databaseHost).to.eql(`${database.host}:${database.port}`); + expect(res.body.firestoreHost).to.eql(`${firestore.host}:${firestore.port}`); + expect(res.body.authHost).to.eql(`${auth.host}:${auth.port}`); + }); + }).timeout(TIMEOUT_MED); + + it("should return an emulated databaseURL when RTDB emulator is running", async () => { + const database = await startFakeEmulator(Emulators.DATABASE); + + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (_req: express.Request, res: express.Response) => { + res.json(JSON.parse(process.env.FIREBASE_CONFIG!)); }, - }, - }, + ), + }; }); - }); - }).timeout(TIMEOUT_LONG); - it("should preserve the Authorization header for callable auth", async () => { - useFunctions(() => { - return { - callable_function_id: require("firebase-functions").https.onCall((data: any, ctx: any) => { + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId`) + .expect(200) + .then((res) => { + expect(res.body.databaseURL).to.eql( + `http://${database.host}:${database.port}/?ns=${TEST_PROJECT_ID}-default-rtdb`, + ); + }); + }).timeout(TIMEOUT_MED); + + it("should return a real databaseURL when RTDB emulator is not running", async () => { + await useFunction(emu, "functionId", () => { return { - header: ctx.rawRequest.headers["authorization"], + functionId: require("firebase-functions").https.onRequest( + (_req: express.Request, res: express.Response) => { + res.json(JSON.parse(process.env.FIREBASE_CONFIG!)); + }, + ), }; - }), - }; + }); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId`) + .expect(200) + .then((res) => { + expect(res.body.databaseURL).to.eql( + `https://${TEST_PROJECT_ID}-default-rtdb.firebaseio.com`, + ); + }); + }).timeout(TIMEOUT_MED); + + it("should report GMT time zone", async () => { + await useFunction(emu, "functionId", () => { + return { + functionId: require("firebase-functions").https.onRequest( + (_req: express.Request, res: express.Response) => { + const now = new Date(); + res.json({ offset: now.getTimezoneOffset() }); + }, + ), + }; + }); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId`) + .expect(200) + .then((res) => { + expect(res.body.offset).to.eql(0); + }); + }).timeout(TIMEOUT_MED); }); - const authHeader = - "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw"; - - // For token info: - // https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmODhiODE0MjljYzQ1MWEzMzVjMmY1Y2RiM2RmYjM0ZWIzYmJjN2YiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXItZHVtcHN0ZXIiLCJhdWQiOiJmaXItZHVtcHN0ZXIiLCJhdXRoX3RpbWUiOjE1ODUwNTMyNjQsInVzZXJfaWQiOiJTbW56OE8xcmxkZmptZHg4QVJVdE12WG1tdzYyIiwic3ViIjoiU21uejhPMXJsZGZqbWR4OEFSVXRNdlhtbXc2MiIsImlhdCI6MTU4NTA1MzI2NCwiZXhwIjoxNTg1MDU2ODY0LCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7fSwic2lnbl9pbl9wcm92aWRlciI6ImFub255bW91cyJ9fQ.ujOthXwov9NJAOmJfumkDzMQgj8P1YRWkhFeq_HqHpPmth1BbtrQ_duwFoFmAPGjnGTuozUi0YUl8eKh4p2CqXi-Wf_OLSumxNnJWhj_tm7OvYWjvUy0ZvjilPBrhQ17_lRnhyOVSLSXfneqehYvE85YkBkFy3GtOpN49fRdmBT7B71Yx8E8SM7fohlia-ah7_uSNpuJXzQ9-0rv6HH9uBYCmjUxb9MiuKwkIjDoYtjTuaqG8-4w8bPrKHmg6V7HeDSNItUcfDbALZiTsM5uob_uuVTwjCCQnwryB5Y3bmdksTqCvp8U7ZTU04HS9CJawTa-zuDXIwlOvsC-J8oQQw - await supertest(functionsEmulator.createHubServer()) - .post("/fake-project-id/us-central1/callable_function_id") - .set({ - "Content-Type": "application/json", - Authorization: authHeader, - }) - .send({ data: {} }) - .expect(200) - .then((res) => { - expect(res.body).to.deep.equal({ - result: { - header: authHeader, + describe("user-defined environment variables", () => { + let cleanup: (() => Promise) | undefined; + + afterEach(async () => { + await cleanup?.(); + cleanup = undefined; + }); + + it("should load environment variables in .env file", async () => { + cleanup = await setupEnvFiles({ + ".env": "FOO=foo\nBAR=bar", + }); + + await useFunction( + emu, + "dotenv", + () => { + return { + dotenv: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + FOO: process.env.FOO, + BAR: process.env.BAR, + }); + }, + ), + }; }, + ["us-central1"], + ); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/dotenv`) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ FOO: "foo", BAR: "bar" }); + }); + }); + + it("should prefer environment variables in .env.{projectId} file", async () => { + cleanup = await setupEnvFiles({ + ".env": "FOO=foo", + [`.env.${TEST_PROJECT_ID}`]: "FOO=goo", }); + + await useFunction( + emu, + "dotenv", + () => { + return { + dotenv: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + FOO: process.env.FOO, + }); + }, + ), + }; + }, + ["us-central1"], + ); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/dotenv`) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ FOO: "goo" }); + }); }); - }).timeout(TIMEOUT_LONG); - describe("environment variables", () => { - let emulatorRegistryStub: sinon.SinonStub; + it("should prefer environment variables in .env.local file", async () => { + cleanup = await setupEnvFiles({ + ".env": "FOO=foo", + [`.env.${TEST_PROJECT_ID}`]: "FOO=goo", + ".env.local": "FOO=hoo", + }); - beforeEach(() => { - emulatorRegistryStub = sinon.stub(registry.EmulatorRegistry, "getInfo").returns(undefined); - }); + await useFunction( + emu, + "dotenv", + () => { + return { + dotenv: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + FOO: process.env.FOO, + }); + }, + ), + }; + }, + ["us-central1"], + ); - afterEach(() => { - emulatorRegistryStub.restore(); + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/dotenv`) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ FOO: "hoo" }); + }); + }); }); - it("should set FIREBASE_DATABASE_EMULATOR_HOST when the emulator is running", async () => { - emulatorRegistryStub.withArgs(Emulators.DATABASE).returns({ - name: Emulators.DATABASE, - host: "localhost", - port: 9090, + describe("secrets", () => { + let readFileSyncStub: sinon.SinonStub; + let accessSecretVersionStub: sinon.SinonStub; + + beforeEach(() => { + readFileSyncStub = sinon.stub(fs, "readFileSync").throws("Unexpected call"); + accessSecretVersionStub = sinon + .stub(secretManager, "accessSecretVersion") + .rejects("Unexpected call"); }); - useFunctions(() => { - return { - function_id: require("firebase-functions").https.onRequest( - (_req: express.Request, res: express.Response) => { - res.json({ - var: process.env.FIREBASE_DATABASE_EMULATOR_HOST, - }); - } - ), - }; + afterEach(() => { + readFileSyncStub.restore(); + accessSecretVersionStub.restore(); }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id") - .expect(200) - .then((res) => { - expect(res.body.var).to.eql("localhost:9090"); - }); - }).timeout(TIMEOUT_MED); + it("should load secret values from local secrets file if one exists", async () => { + readFileSyncStub.returns("MY_SECRET=local"); + + await useFunction( + emu, + "secretsFunctionId", + () => { + return { + secretsFunctionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ secret: process.env.MY_SECRET }); + }, + ), + }; + }, + ["us-central1"], + { + secretEnvironmentVariables: [ + { + projectId: TEST_PROJECT_ID, + secret: "MY_SECRET", + key: "MY_SECRET", + version: "1", + }, + ], + }, + ); - it("should set FIRESTORE_EMULATOR_HOST when the emulator is running", async () => { - emulatorRegistryStub.withArgs(Emulators.FIRESTORE).returns({ - name: Emulators.FIRESTORE, - host: "localhost", - port: 9090, + await supertest(emu.createHubServer()) + .get("/" + TEST_PROJECT_ID + "/us-central1/secretsFunctionId") + .expect(200) + .then((res) => { + expect(res.body.secret).to.equal("local"); + }); }); - useFunctions(() => { - return { - function_id: require("firebase-functions").https.onRequest( - (_req: express.Request, res: express.Response) => { - res.json({ - var: process.env.FIRESTORE_EMULATOR_HOST, - }); - } - ), - }; + it("should try to access secret values from Secret Manager", async () => { + readFileSyncStub.throws({ code: "ENOENT" }); + accessSecretVersionStub.resolves("secretManager"); + + await useFunction( + emu, + "secretsFunctionId", + () => { + return { + secretsFunctionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ secret: process.env.MY_SECRET }); + }, + ), + }; + }, + ["us-central1"], + { + secretEnvironmentVariables: [ + { + projectId: TEST_PROJECT_ID, + secret: "MY_SECRET", + key: "MY_SECRET", + version: "1", + }, + ], + }, + ); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/secretsFunctionId`) + .expect(200) + .then((res) => { + expect(res.body.secret).to.equal("secretManager"); + }); }); + }); + }); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id") - .expect(200) - .then((res) => { - expect(res.body.var).to.eql("localhost:9090"); - }); + describe("Discover", () => { + let cleanupSource: (() => Promise) | undefined; + let cleanupEnvs: (() => Promise) | undefined; + + afterEach(async () => { + await cleanupSource?.(); + await cleanupEnvs?.(); + }); + + it("resolves function with parameter value defined in .env correctly", async () => { + cleanupSource = await writeSource( + () => { + return { + functionId: require("firebase-functions") + .runWith({ timeoutSeconds: "__$timeout__" }) + .https.onRequest((req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }), + }; + }, + { + timeout: () => require("firebase-functions/params").defineInt("TIMEOUT"), + }, + ); + cleanupEnvs = await setupEnvFiles({ + ".env": "TIMEOUT=24", + }); + + const triggerDefinitions = await emu.discoverTriggers(TEST_BACKEND); + expect(triggerDefinitions).to.have.length(1); + expect(triggerDefinitions[0].timeoutSeconds).to.equal(24); }); - it("should set FIREBASE_AUTH_EMULATOR_HOST when the emulator is running", async () => { - emulatorRegistryStub.withArgs(Emulators.AUTH).returns({ - name: Emulators.FIRESTORE, - host: "localhost", - port: 9099, + it("resolves function with parameter value defined in .env.projectId correctly", async () => { + cleanupSource = await writeSource( + () => { + return { + functionId: require("firebase-functions") + .runWith({ timeoutSeconds: "__$timeout__" }) + .https.onRequest((req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }), + }; + }, + { + timeout: () => require("firebase-functions/params").defineInt("TIMEOUT"), + }, + ); + cleanupEnvs = await setupEnvFiles({ + ".env": "TIMEOUT=24", + [`.env.${TEST_PROJECT_ID}`]: "TIMEOUT=25", }); - useFunctions(() => { + const triggerDefinitions = await emu.discoverTriggers(TEST_BACKEND); + expect(triggerDefinitions).to.have.length(1); + expect(triggerDefinitions[0].timeoutSeconds).to.equal(25); + }); + + it("resolves function with parameter value defined in .env.local correctly", async () => { + cleanupSource = await writeSource( + () => { + return { + functionId: require("firebase-functions") + .runWith({ timeoutSeconds: "__$timeout__" }) + .https.onRequest((req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }), + }; + }, + { + timeout: () => require("firebase-functions/params").defineInt("TIMEOUT"), + }, + ); + cleanupEnvs = await setupEnvFiles({ + ".env": "TIMEOUT=24", + [`.env.${TEST_PROJECT_ID}`]: "TIMEOUT=25", + ".env.local": "TIMEOUT=26", + }); + + const triggerDefinitions = await emu.discoverTriggers(TEST_BACKEND); + expect(triggerDefinitions).to.have.length(1); + expect(triggerDefinitions[0].timeoutSeconds).to.equal(26); + }); + }); + + it("should enforce timeout", async () => { + await useFunction( + emu, + "timeoutFn", + () => { return { - function_id: require("firebase-functions").https.onRequest( - (_req: express.Request, res: express.Response) => { - res.json({ - var: process.env.FIREBASE_AUTH_EMULATOR_HOST, + timeoutFn: require("firebase-functions") + .runWith({ timeoutSeconds: 1 }) + .https.onRequest((req: express.Request, res: express.Response): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + res.sendStatus(200); + resolve(); + }, 5_000); }); - } - ), + }), }; - }); + }, + ["us-central1"], + { + timeoutSeconds: 1, + }, + ); - await supertest(functionsEmulator.createHubServer()) - .get("/fake-project-id/us-central1/function_id") - .expect(200) - .then((res) => { - expect(res.body.var).to.eql("localhost:9099"); - }); - }).timeout(TIMEOUT_MED); + await supertest(emu.createHubServer()) + .get("/fake-project-id/us-central1/timeoutFn") + .expect(500); }); }); diff --git a/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts b/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts index 1a2888a178b..1ca2ad0535b 100644 --- a/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts +++ b/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts @@ -1,959 +1,798 @@ -import { Change } from "firebase-functions"; -import { DocumentSnapshot } from "firebase-functions/lib/providers/firestore"; import { expect } from "chai"; -import { IncomingMessage, request } from "http"; -import * as _ from "lodash"; + +import * as http from "http"; +import * as fs from "fs/promises"; +import * as spawn from "cross-spawn"; +import * as path from "path"; +import { ChildProcess } from "child_process"; + import * as express from "express"; -import * as fs from "fs"; -import * as sinon from "sinon"; - -import { EmulatorLog, Emulators } from "../../src/emulator/types"; -import { FunctionRuntimeBundles, TIMEOUT_LONG, TIMEOUT_MED, MODULE_ROOT } from "./fixtures"; -import { FunctionsRuntimeBundle, SignatureType } from "../../src/emulator/functionsEmulatorShared"; -import { InvokeRuntimeOpts, FunctionsEmulator } from "../../src/emulator/functionsEmulator"; -import { RuntimeWorker } from "../../src/emulator/functionsRuntimeWorker"; +import { Change } from "firebase-functions"; +import { DocumentSnapshot } from "firebase-functions/v1/firestore"; + +import { FunctionRuntimeBundles, TIMEOUT_LONG, MODULE_ROOT } from "./fixtures"; +import { + FunctionsRuntimeBundle, + getTemporarySocketPath, + SignatureType, +} from "../../src/emulator/functionsEmulatorShared"; import { streamToString } from "../../src/utils"; -import * as registry from "../../src/emulator/registry"; -const DO_NOTHING = () => { - // do nothing. +const FUNCTIONS_DIR = `./scripts/emulator-tests/functions`; +const ADMIN_SDK_CONFIG = { + projectId: "fake-project-id", + databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", + storageBucket: "fake-project-id.appspot.com", }; -const functionsEmulator = new FunctionsEmulator({ - projectId: "fake-project-id", - functionsDir: MODULE_ROOT, -}); -(functionsEmulator as any).adminSdkConfig = FunctionRuntimeBundles.onRequest.adminSdkConfig; -functionsEmulator.nodeBinary = process.execPath; +interface Runtime { + proc: ChildProcess; + port: string; + rawMsg: string[]; + sysMsg: Record; + stdout: string[]; +} -async function countLogEntries(worker: RuntimeWorker): Promise<{ [key: string]: number }> { - const runtime = worker.runtime; - const counts: { [key: string]: number } = {}; +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); - runtime.events.on("log", (el: EmulatorLog) => { - counts[el.type] = (counts[el.type] || 0) + 1; +async function isSocketReady(socketPath: string): Promise { + return new Promise((resolve, reject) => { + const req = http + .request( + { + method: "GET", + path: "/__/health", + socketPath, + }, + () => resolve(), + ) + .end(); + req.on("error", (error) => { + reject(error); + }); }); +} - await runtime.exit; - return counts; +async function waitForSocketReady(socketPath: string): Promise { + const timeout = new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("Timeout - runtime server not ready")); + }, 10_000); + }); + while (true) { + try { + await Promise.race([isSocketReady(socketPath), timeout]); + break; + } catch (err: any) { + // Allow us to wait until the server is listening. + if (["ECONNREFUSED", "ENOENT"].includes(err?.code)) { + await sleep(100); + continue; + } + throw err; + } + } } -function startRuntimeWithFunctions( - frb: FunctionsRuntimeBundle, - triggers: () => {}, +async function startRuntime( + triggerName: string, signatureType: SignatureType, - opts?: InvokeRuntimeOpts -): RuntimeWorker { - const serializedTriggers = triggers.toString(); - - opts = opts || { nodeBinary: process.execPath }; - opts.ignore_warnings = true; - opts.serializedTriggers = serializedTriggers; - - return functionsEmulator.startFunctionRuntime( - frb.triggerId!, - frb.targetName!, - signatureType, - frb.proto, - opts - ); + triggerSource: () => {}, + runtimeEnvs?: Record, +): Promise { + const env: Record = { ...runtimeEnvs }; + env.GCLOUD_PROJECT = ADMIN_SDK_CONFIG.projectId; + env.FUNCTION_TARGET = triggerName; + env.FUNCTION_SIGNATURE_TYPE = signatureType; + env.PORT = getTemporarySocketPath(); + + env.FIREBASE_CONFIG = JSON.stringify(ADMIN_SDK_CONFIG); + env.FUNCTIONS_EMULATOR = "true"; + env.FIREBASE_DEBUG_MODE = "true"; + env.FIREBASE_DEBUG_FEATURES = JSON.stringify({ + skipTokenVerification: true, + enableCors: true, + }); + + const sourceCode = `module.exports = (${triggerSource.toString()})();\n`; + await fs.writeFile(`${FUNCTIONS_DIR}/index.js`, sourceCode); + + const args = [path.join(MODULE_ROOT, "src", "emulator", "functionsEmulatorRuntime")]; + const proc = spawn(process.execPath, args, { + env: { ...process.env, ...env }, + cwd: FUNCTIONS_DIR, + stdio: ["pipe", "pipe", "pipe", "ipc"], + }); + + const runtime: Runtime = { + proc, + rawMsg: [], + sysMsg: {}, + stdout: [], + port: env.PORT, + }; + + proc.on("message", (message) => { + const msg = message.toString(); + runtime.rawMsg.push(msg); + try { + const m = JSON.parse(msg); + if (m.type) { + runtime.sysMsg[m.type] = runtime.sysMsg[m.type] || []; + runtime.sysMsg[m.type].push(`text: ${m.text};data: ${JSON.stringify(m.data)}`); + } + } catch { + // Carry on; + } + }); + + proc.stdout?.on("data", (data) => { + runtime.stdout.push(data.toString()); + }); + + proc.stderr?.on("data", (data) => { + runtime.stdout.push(data.toString()); + }); + + await waitForSocketReady(env.PORT); + return runtime; } -/** - * Three step process: - * 1) Wait for the runtime to be ready. - * 2) Call the runtime with the specified bundle and collect all data. - * 3) Wait for the runtime to exit - */ -async function callHTTPSFunction( - worker: RuntimeWorker, - frb: FunctionsRuntimeBundle, - options: { path?: string; headers?: { [key: string]: string } } = {}, - requestData?: string -): Promise { - await worker.waitForSocketReady(); - - if (!worker.lastArgs) { - throw new Error("Can't talk to worker with undefined args"); - } +interface ReqOpts { + data?: string; + path?: string; + method?: string; + headers?: Record; +} - const socketPath = worker.lastArgs.frb.socketPath; - const path = options.path || "/"; +function sendEvent(runtime: Runtime, proto: any): Promise { + const reqData = JSON.stringify(proto); + return sendReq(runtime, { + data: reqData, + headers: { + "Content-Type": "application/json", + "Content-Length": `${reqData.length}`, + }, + }); +} - const res = await new Promise((resolve, reject) => { - const req = request( +async function sendReq(runtime: Runtime, opts: ReqOpts = {}): Promise { + const path = opts.path || "/"; + const res = await new Promise((resolve, reject) => { + const req = http.request( { - method: "POST", - headers: options.headers, - socketPath, + method: opts.method || "POST", + headers: opts.headers, + socketPath: runtime.port, path, }, - resolve + resolve, ); req.on("error", reject); - if (requestData) { - req.write(requestData); + if (opts.data) { + req.write(opts.data); } req.end(); }); - const result = await streamToString(res); - await worker.runtime.exit; - return result; } -describe("FunctionsEmulator-Runtime", () => { - describe("Stubs, Mocks, and Helpers (aka Magic, Glee, and Awesomeness)", () => { - describe("_InitializeNetworkFiltering(...)", () => { - it("should log outgoing unknown HTTP requests via 'http'", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(async () => { - await new Promise((resolve) => { - console.log(require("http").get.toString()); - require("http").get("http://example.com", resolve); - }); - }), - }; - }, - "event" - ); - - const logs = await countLogEntries(worker); - expect(logs["unidentified-network-access"]).to.gte(1); - }).timeout(TIMEOUT_LONG); - - it("should log outgoing unknown HTTP requests via 'https'", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(async () => { - await new Promise((resolve) => { - require("https").get("https://example.com", resolve); - }); - }), - }; - }, - "event" - ); - - const logs = await countLogEntries(worker); - - expect(logs["unidentified-network-access"]).to.gte(1); - }).timeout(TIMEOUT_LONG); +async function sendDebugBundle(runtime: Runtime, debug: FunctionsRuntimeBundle["debug"]) { + return new Promise((resolve) => { + runtime.proc.send(JSON.stringify(debug), resolve); + }); +} - it("should log outgoing Google API requests", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(async () => { - await new Promise((resolve) => { - require("https").get("https://storage.googleapis.com", resolve); - }); - }), - }; - }, - "event" - ); +describe("FunctionsEmulator-Runtime", function () { + // eslint-disable-next-line @typescript-eslint/no-invalid-this + this.timeout(TIMEOUT_LONG); - const logs = await countLogEntries(worker); + let runtime: Runtime | undefined; - expect(logs["googleapis-network-access"]).to.gte(1); - }).timeout(TIMEOUT_LONG); - }); + afterEach(() => { + runtime?.proc.kill(9); + runtime = undefined; + }); - describe("_InitializeFirebaseAdminStubs(...)", () => { - let emulatorRegistryStub: sinon.SinonStub; + describe("Stubs, Mocks, and Helpers", () => { + describe("_InitializeNetworkFiltering", () => { + it("should log outgoing unknown HTTP requests via 'http'", async () => { + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(async () => { + await new Promise((resolve) => { + require("http").get("http://example.com", resolve); + }); + }), + }; + }); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["unidentified-network-access"]?.length).to.gte(1); + }); - beforeEach(() => { - emulatorRegistryStub = sinon.stub(registry.EmulatorRegistry, "getInfo").returns(undefined); + it("should log outgoing unknown HTTP requests via 'https'", async () => { + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(async () => { + await new Promise((resolve) => { + require("https").get("https://example.com", resolve); + }); + }), + }; + }); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["unidentified-network-access"]?.length).to.gte(1); }); - afterEach(() => { - emulatorRegistryStub.restore(); + it("should log outgoing Google API requests", async () => { + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(async () => { + await new Promise((resolve) => { + require("https").get("https://storage.googleapis.com", resolve); + }); + }), + }; + }); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["googleapis-network-access"]?.length).to.gte(1); }); + }); + describe("_InitializeFirebaseAdminStubs(...)", () => { it("should provide stubbed default app from initializeApp", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(DO_NOTHING), - }; - }, - "event" - ); - - const logs = await countLogEntries(worker); - expect(logs["default-admin-app-used"]).to.eq(1); - }).timeout(TIMEOUT_MED); + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(() => { + console.log("hello world"); + }), + }; + }); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["default-admin-app-used"]?.length).to.gte(1); + }); it("should provide a stubbed app with custom options", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp({ - custom: true, - }); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(DO_NOTHING), - }; - }, - "event" - ); - - let foundMatch = false; - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "SYSTEM" || el.type !== "default-admin-app-used") { - return; - } - - foundMatch = true; - expect(el.data).to.eql({ opts: { custom: true } }); + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp({ custom: true }); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(() => { + console.log("hello world"); + }), + }; }); - - await worker.runtime.exit; - expect(foundMatch).to.be.true; - }).timeout(TIMEOUT_MED); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["default-admin-app-used"]?.length).to.gte(1); + expect(runtime.sysMsg["default-admin-app-used"]?.join(" ")).to.match(/"custom":true/); + }); it("should provide non-stubbed non-default app from initializeApp", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp(); // We still need to initialize default for snapshots - require("firebase-admin").initializeApp({}, "non-default"); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(DO_NOTHING), - }; - }, - "event" - ); - const logs = await countLogEntries(worker); - expect(logs["non-default-admin-app-used"]).to.eq(1); - }).timeout(TIMEOUT_MED); - - it("should route all sub-fields accordingly", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(() => { - console.log( - JSON.stringify(require("firebase-admin").firestore.FieldValue.increment(4)) - ); - return Promise.resolve(); - }), - }; - }, - "event" - ); - - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - - expect(JSON.parse(el.text)).to.deep.eq({ operand: 4 }); + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); // We still need to initialize default for snapshots + require("firebase-admin").initializeApp({}, "non-default"); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(() => { + console.log("hello world"); + }), + }; }); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["non-default-admin-app-used"]?.length).to.gte(1); + }); - const logs = await countLogEntries(worker); - expect(logs["function-log"]).to.eq(1); - }).timeout(TIMEOUT_MED); - - it("should expose Firestore prod when the emulator is not running", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - const admin = require("firebase-admin"); - admin.initializeApp(); - - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json(admin.firestore()._settings); + it("should route all sub-fields accordingly", async () => { + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(() => { + console.log( + JSON.stringify(require("firebase-admin/firestore").FieldValue.increment(4)), + ); return Promise.resolve(); }), - }; - }, - "http" - ); + }; + }); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.stdout.join(" ")).to.match(/{"operand":4}/); + }); - const data = await callHTTPSFunction(worker, frb); + it("should expose Firestore prod when the emulator is not running", async () => { + runtime = await startRuntime("functionId", "http", () => { + const admin = require("firebase-admin"); + admin.initializeApp(); + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json(admin.firestore()._settings); + return Promise.resolve(); + }), + }; + }); + const data = await sendReq(runtime); const info = JSON.parse(data); - expect(info.projectId).to.eql("fake-project-id"); expect(info.servicePath).to.be.undefined; expect(info.port).to.be.undefined; - }).timeout(TIMEOUT_MED); + }); it("should expose a stubbed Firestore when the emulator is running", async () => { - const frb = FunctionRuntimeBundles.onRequest; - emulatorRegistryStub.withArgs(Emulators.FIRESTORE).returns({ - name: Emulators.DATABASE, - host: "localhost", - port: 9090, - }); - - const worker = startRuntimeWithFunctions( - frb, + runtime = await startRuntime( + "functionId", + "http", () => { const admin = require("firebase-admin"); admin.initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { res.json(admin.firestore()._settings); return Promise.resolve(); }), }; }, - "http" + { FIRESTORE_EMULATOR_HOST: "localhost:9090" }, ); - - const data = await callHTTPSFunction(worker, frb); + const data = await sendReq(runtime); const info = JSON.parse(data); - expect(info.projectId).to.eql("fake-project-id"); expect(info.servicePath).to.eq("localhost"); expect(info.port).to.eq(9090); - }).timeout(TIMEOUT_MED); + }); it("should expose RTDB prod when the emulator is not running", async () => { - const frb = FunctionRuntimeBundles.onRequest; - - const worker = startRuntimeWithFunctions( - frb, - () => { - const admin = require("firebase-admin"); - admin.initializeApp(); - - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json({ - url: admin.database().ref().toString(), - }); - }), - }; - }, - "http" - ); - - const data = await callHTTPSFunction(worker, frb); + runtime = await startRuntime("functionId", "http", () => { + const admin = require("firebase-admin"); + admin.initializeApp(); + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json({ + url: admin.database().ref().toString(), + }); + }), + }; + }); + const data = await sendReq(runtime); const info = JSON.parse(data); expect(info.url).to.eql("https://fake-project-id-default-rtdb.firebaseio.com/"); - }).timeout(TIMEOUT_MED); + }); it("should expose a stubbed RTDB when the emulator is running", async () => { - const frb = FunctionRuntimeBundles.onRequest; - emulatorRegistryStub.withArgs(Emulators.DATABASE).returns({ - name: Emulators.DATABASE, - host: "localhost", - port: 9090, - }); - - const worker = startRuntimeWithFunctions( - frb, + runtime = await startRuntime( + "functionId", + "http", () => { const admin = require("firebase-admin"); admin.initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { res.json({ url: admin.database().ref().toString(), }); }), }; }, - "http" - ); - - const data = await callHTTPSFunction(worker, frb); - const info = JSON.parse(data); - expect(info.url).to.eql("http://localhost:9090/"); - }).timeout(TIMEOUT_MED); - - it("should return an emulated databaseURL when RTDB emulator is running", async () => { - const frb = FunctionRuntimeBundles.onRequest; - emulatorRegistryStub.withArgs(Emulators.DATABASE).returns({ - name: Emulators.DATABASE, - host: "localhost", - port: 9090, - }); - - const worker = startRuntimeWithFunctions( - frb, - () => { - const admin = require("firebase-admin"); - admin.initializeApp(); - - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json(JSON.parse(process.env.FIREBASE_CONFIG!)); - }), - }; - }, - "http" - ); - - const data = await callHTTPSFunction(worker, frb); - const info = JSON.parse(data); - expect(info.databaseURL).to.eql(`http://localhost:9090/?ns=fake-project-id-default-rtdb`); - }).timeout(TIMEOUT_MED); - - it("should return a real databaseURL when RTDB emulator is not running", async () => { - const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest); - const worker = startRuntimeWithFunctions( - frb, - () => { - const admin = require("firebase-admin"); - admin.initializeApp(); - - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json(JSON.parse(process.env.FIREBASE_CONFIG!)); - }), - }; + { + FIREBASE_DATABASE_EMULATOR_HOST: "localhost:9090", }, - "http" ); - - const data = await callHTTPSFunction(worker, frb); + const data = await sendReq(runtime); const info = JSON.parse(data); - expect(info.databaseURL).to.eql(frb.adminSdkConfig.databaseURL!); - }).timeout(TIMEOUT_MED); + expect(info.url).to.eql("http://localhost:9090/"); + }); }); }); - describe("_InitializeFunctionsConfigHelper()", () => { - before(() => { - fs.writeFileSync( - MODULE_ROOT + "/.runtimeconfig.json", - '{"real":{"exist":"already exists" }}' - ); + const cfgPath = path.join(FUNCTIONS_DIR, ".runtimeconfig.json"); + + before(async () => { + await fs.writeFile(cfgPath, '{"real":{"exist":"already exists" }}'); }); - after(() => { - fs.unlinkSync(MODULE_ROOT + "/.runtimeconfig.json"); + after(async () => { + await fs.unlink(cfgPath); }); it("should tell the user if they've accessed a non-existent function field", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onCreate, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(() => { - // Exists - console.log(require("firebase-functions").config().real); - - // Does not exist - console.log(require("firebase-functions").config().foo); - console.log(require("firebase-functions").config().bar); - }), - }; - }, - "event" - ); - - const logs = await countLogEntries(worker); - expect(logs["functions-config-missing-value"]).to.eq(2); - }).timeout(TIMEOUT_MED); + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate(() => { + // Exists + console.log(require("firebase-functions").config().real); + // Does not exist + console.log(require("firebase-functions").config().foo); + console.log(require("firebase-functions").config().bar); + }), + }; + }); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["functions-config-missing-value"]?.length).to.eq(2); + }); }); - describe("Runtime", () => { describe("HTTPS", () => { it("should handle a GET request", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json({ from_trigger: true }); - }), - }; - }, - "http" - ); - - const data = await callHTTPSFunction(worker, frb); + runtime = await startRuntime("functionId", "http", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json({ from_trigger: true }); + }), + }; + }); + const data = await sendReq(runtime, { method: "GET" }); expect(JSON.parse(data)).to.deep.equal({ from_trigger: true }); - }).timeout(TIMEOUT_MED); + }); it("should handle a POST request with form data", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json(req.body); - }), - }; - }, - "http" - ); + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json(req.body); + }), + }; + }); const reqData = "name=sparky"; - const data = await callHTTPSFunction( - worker, - frb, - { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Length": `${reqData.length}`, - }, + const data = await sendReq(runtime, { + data: reqData, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": `${reqData.length}`, }, - reqData - ); - + }); expect(JSON.parse(data)).to.deep.equal({ name: "sparky" }); - }).timeout(TIMEOUT_MED); + }); it("should handle a POST request with JSON data", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json(req.body); - }), - }; - }, - "http" - ); + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json(req.body); + }), + }; + }); const reqData = '{"name": "sparky"}'; - const data = await callHTTPSFunction( - worker, - frb, - { - headers: { - "Content-Type": "application/json", - "Content-Length": `${reqData.length}`, - }, + const data = await sendReq(runtime, { + data: reqData, + headers: { + "Content-Type": "application/json", + "Content-Length": `${reqData.length}`, }, - reqData - ); - + }); expect(JSON.parse(data)).to.deep.equal({ name: "sparky" }); - }).timeout(TIMEOUT_MED); + }); it("should handle a POST request with text data", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json(req.body); - }), - }; - }, - "http" - ); + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json(req.body); + }), + }; + }); const reqData = "name is sparky"; - const data = await callHTTPSFunction( - worker, - frb, - { - headers: { - "Content-Type": "text/plain", - "Content-Length": `${reqData.length}`, - }, + const data = await sendReq(runtime, { + data: reqData, + headers: { + "Content-Type": "text/plain", + "Content-Length": `${reqData.length}`, }, - reqData - ); - + }); expect(JSON.parse(data)).to.deep.equal("name is sparky"); - }).timeout(TIMEOUT_MED); + }); it("should handle a POST request with any other type", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json(req.body); - }), - }; - }, - "http" - ); + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json(req.body); + }), + }; + }); const reqData = "name is sparky"; - const data = await callHTTPSFunction( - worker, - frb, - { - headers: { - "Content-Type": "gibber/ish", - "Content-Length": `${reqData.length}`, - }, + const data = await sendReq(runtime, { + data: reqData, + headers: { + "Content-Type": "gibber/ish", + "Content-Length": `${reqData.length}`, }, - reqData - ); - + }); expect(JSON.parse(data).type).to.deep.equal("Buffer"); expect(JSON.parse(data).data.length).to.deep.equal(14); - }).timeout(TIMEOUT_MED); + }); it("should handle a POST request and store rawBody", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.send(req.rawBody); - }), - }; - }, - "http" - ); + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.send(req.rawBody); + }), + }; + }); - const reqData = "How are you?"; - const data = await callHTTPSFunction( - worker, - frb, - { - headers: { - "Content-Type": "gibber/ish", - "Content-Length": `${reqData.length}`, - }, + const reqData = "name is sparky"; + const data = await sendReq(runtime, { + data: reqData, + headers: { + "Content-Type": "gibber/ish", + "Content-Length": `${reqData.length}`, }, - reqData - ); - + }); expect(data).to.equal(reqData); - }).timeout(TIMEOUT_MED); + }); it("should forward request to Express app", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - const app = require("express")(); - app.all("/", (req: express.Request, res: express.Response) => { - res.json({ - hello: req.header("x-hello"), - }); + runtime = await startRuntime("functionId", "http", () => { + const app = require("express")(); + app.all("/", (req: express.Request, res: express.Response) => { + res.json({ + hello: req.header("x-hello"), }); - return { - function_id: require("firebase-functions").https.onRequest(app), - }; - }, - "http" - ); + }); + return { + functionId: require("firebase-functions").https.onRequest(app), + }; + }); - const data = await callHTTPSFunction(worker, frb, { + const reqData = "name is sparky"; + const data = await sendReq(runtime, { + data: reqData, headers: { "x-hello": "world", }, }); - expect(JSON.parse(data)).to.deep.equal({ hello: "world" }); - }).timeout(TIMEOUT_MED); + }); it("should handle `x-forwarded-host`", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json({ hostname: req.hostname }); - }), - }; - }, - "http" - ); + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.json({ hostname: req.hostname }); + }), + }; + }); - const data = await callHTTPSFunction(worker, frb, { + const reqData = "name is sparky"; + const data = await sendReq(runtime, { + data: reqData, headers: { "x-forwarded-host": "real-hostname", }, }); - expect(JSON.parse(data)).to.deep.equal({ hostname: "real-hostname" }); - }).timeout(TIMEOUT_MED); - - it("should report GMT time zone", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - const now = new Date(); - res.json({ offset: now.getTimezoneOffset() }); - }), - }; - }, - "http" - ); - - const data = await callHTTPSFunction(worker, frb); - expect(JSON.parse(data)).to.deep.equal({ offset: 0 }); - }).timeout(TIMEOUT_MED); + }); }); describe("Cloud Firestore", () => { it("should provide Change for firestore.onWrite()", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onWrite, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onWrite((change: Change) => { - console.log( - JSON.stringify({ - before_exists: change.before.exists, - after_exists: change.after.exists, - }) - ); - return Promise.resolve(); - }), - }; - }, - "event" - ); - - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - - expect(JSON.parse(el.text)).to.deep.eq({ before_exists: false, after_exists: true }); + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onWrite((change: Change) => { + console.log( + JSON.stringify({ + before_exists: change.before.exists, + after_exists: change.after.exists, + }), + ); + return Promise.resolve(); + }), + }; }); - const logs = await countLogEntries(worker); - expect(logs["function-log"]).to.eq(1); - }).timeout(TIMEOUT_MED); + await sendEvent(runtime, FunctionRuntimeBundles.onWrite.proto); + expect(runtime.stdout.join(" ")).to.match(/{"before_exists":false,"after_exists":true}/); + }); it("should provide Change for firestore.onUpdate()", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onUpdate, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onUpdate((change: Change) => { - console.log( - JSON.stringify({ - before_exists: change.before.exists, - after_exists: change.after.exists, - }) - ); - return Promise.resolve(); - }), - }; - }, - "event" - ); - - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - expect(JSON.parse(el.text)).to.deep.eq({ before_exists: true, after_exists: true }); + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onUpdate((change: Change) => { + console.log( + JSON.stringify({ + before_exists: change.before.exists, + after_exists: change.after.exists, + }), + ); + return Promise.resolve(); + }), + }; }); - const logs = await countLogEntries(worker); - expect(logs["function-log"]).to.eq(1); - }).timeout(TIMEOUT_MED); - - it("should provide DocumentSnapshot for firestore.onDelete()", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onDelete, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onDelete((snap: DocumentSnapshot) => { - console.log( - JSON.stringify({ - snap_exists: snap.exists, - }) - ); - return Promise.resolve(); - }), - }; - }, - "event" - ); + await sendEvent(runtime, FunctionRuntimeBundles.onUpdate.proto); + expect(runtime.stdout.join(" ")).to.match(/{"before_exists":true,"after_exists":true}/); + }); - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - expect(JSON.parse(el.text)).to.deep.eq({ snap_exists: true }); + it("should provide Change for firestore.onDelete()", async () => { + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onDelete((snap: DocumentSnapshot) => { + console.log( + JSON.stringify({ + snap_exists: snap.exists, + }), + ); + return Promise.resolve(); + }), + }; }); - const logs = await countLogEntries(worker); - expect(logs["function-log"]).to.eq(1); - }).timeout(TIMEOUT_MED); - - it("should provide DocumentSnapshot for firestore.onCreate()", async () => { - const worker = startRuntimeWithFunctions( - FunctionRuntimeBundles.onWrite, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate((snap: DocumentSnapshot) => { - console.log( - JSON.stringify({ - snap_exists: snap.exists, - }) - ); - return Promise.resolve(); - }), - }; - }, - "event" - ); + await sendEvent(runtime, FunctionRuntimeBundles.onDelete.proto); + expect(runtime.stdout.join(" ")).to.match(/{"snap_exists":true}/); + }); - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - expect(JSON.parse(el.text)).to.deep.eq({ snap_exists: true }); + it("should provide Change for firestore.onCreate()", async () => { + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); + return { + functionId: require("firebase-functions") + .firestore.document("test/test") + .onCreate((snap: DocumentSnapshot) => { + console.log( + JSON.stringify({ + snap_exists: snap.exists, + }), + ); + return Promise.resolve(); + }), + }; }); - const logs = await countLogEntries(worker); - expect(logs["function-log"]).to.eq(1); - }).timeout(TIMEOUT_MED); + await sendEvent(runtime, FunctionRuntimeBundles.onUpdate.proto); + expect(runtime.stdout.join(" ")).to.match(/{"snap_exists":true}/); + }); }); describe("Error handling", () => { it("Should handle regular functions for Express handlers", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - throw new Error("not a thing"); - }), - }; - }, - "http" - ); - - const logs = countLogEntries(worker); - + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest(() => { + throw new Error("not a thing"); + }), + }; + }); try { - await callHTTPSFunction(worker, frb); - } catch (e) { - // No-op + await sendReq(runtime); + } catch (e: any) { + // Carry on } - expect((await logs)["runtime-error"]).to.eq(1); - }).timeout(TIMEOUT_MED); + expect(runtime.sysMsg["runtime-error"]?.length).to.eq(1); + }); it("Should handle async functions for Express handlers", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions").https.onRequest( - async (req: any, res: any) => { - await Promise.resolve(); // Required `await` for `async`. - return Promise.reject(new Error("not a thing")); - } - ), - }; - }, - "http" - ); - - const logs = countLogEntries(worker); - + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions").https.onRequest(async () => { + return Promise.reject(new Error("not a thing")); + }), + }; + }); try { - await callHTTPSFunction(worker, frb); - } catch { - // No-op + await sendReq(runtime); + } catch (e: any) { + // Carry on } - expect((await logs)["runtime-error"]).to.eq(1); - }).timeout(TIMEOUT_MED); + expect(runtime.sysMsg["runtime-error"]?.length).to.eq(1); + }); it("Should handle async/runWith functions for Express handlers", async () => { - const frb = FunctionRuntimeBundles.onRequest; - const worker = startRuntimeWithFunctions( - frb, - () => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .runWith({}) - .https.onRequest(async (req: any, res: any) => { - await Promise.resolve(); // Required `await` for `async`. - return Promise.reject(new Error("not a thing")); - }), - }; - }, - "http" - ); - - const logs = countLogEntries(worker); - + runtime = await startRuntime("functionId", "http", () => { + return { + functionId: require("firebase-functions") + .runWith({}) + .https.onRequest(async () => { + return Promise.reject(new Error("not a thing")); + }), + }; + }); try { - await callHTTPSFunction(worker, frb); - } catch { - // No-op + await sendReq(runtime); + } catch (e: any) { + // Carry on } - expect((await logs)["runtime-error"]).to.eq(1); - }).timeout(TIMEOUT_MED); + expect(runtime.sysMsg["runtime-error"]?.length).to.eq(1); + }); + }); + }); + + describe("Debug", () => { + it("handles debug message to change function target", async () => { + runtime = await startRuntime( + "function0", + "http", + () => { + return { + function0: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.send("function0"); + }), + function1: require("firebase-functions").https.onRequest((req: any, res: any) => { + res.send("function1"); + }), + }; + }, + { + FUNCTION_DEBUG_MODE: "true", + }, + ); + await sendDebugBundle(runtime, { functionSignature: "http", functionTarget: "function0" }); + const fn0Res = await sendReq(runtime); + expect(fn0Res).to.equal("function0"); + await sendDebugBundle(runtime, { functionSignature: "http", functionTarget: "function1" }); + const fn1Res = await sendReq(runtime); + expect(fn1Res).to.equal("function1"); + }); + + it("disables configured timeout when in debug mode", async () => { + const timeoutEnvs = { + FUNCTIONS_EMULATOR_TIMEOUT_SECONDS: "1", + FUNCTION_DEBUG_MODE: "true", + }; + runtime = await startRuntime( + "functionId", + "http", + () => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: any, resp: any): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resp.sendStatus(200); + resolve(); + }, 3_000); + }); + }, + ), + }; + }, + timeoutEnvs, + ); + try { + await sendDebugBundle(runtime, { + functionSignature: "http", + functionTarget: "functionId", + }); + await sendReq(runtime); + } catch (e: any) { + // Carry on + } + expect(runtime.sysMsg["runtime-error"]).to.be.undefined; }); }); }); diff --git a/scripts/emulator-tests/run.sh b/scripts/emulator-tests/run.sh index 0c2023c1d5e..4b140f106f1 100755 --- a/scripts/emulator-tests/run.sh +++ b/scripts/emulator-tests/run.sh @@ -14,8 +14,8 @@ trap cleanup EXIT # Need to copy `package.json` to the directory so it can be referenced in code. cp package.json dev/package.json +# Install deps required to run test triggers. +(cd scripts/emulator-tests/functions && npm ci) + # Run the tests from the built dev directory. -mocha \ - --require ts-node/register \ - --require src/test/helpers/mocha-bootstrap.ts \ - dev/scripts/emulator-tests/*.spec.* + mocha dev/scripts/emulator-tests/*.spec.* diff --git a/scripts/emulator-tests/unzipEmulators.spec.ts b/scripts/emulator-tests/unzipEmulators.spec.ts new file mode 100644 index 00000000000..0766bd989b0 --- /dev/null +++ b/scripts/emulator-tests/unzipEmulators.spec.ts @@ -0,0 +1,115 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as path from "path"; +import { unzip } from "../../src/unzip"; +import { DownloadDetails } from "../../src/emulator/downloadableEmulators"; +import { Client } from "../../src/apiv2"; +import { tmpdir } from "os"; + +describe("unzipEmulators", () => { + let tempDir: string; + + before(async () => { + tempDir = await fs.promises.mkdtemp(path.join(tmpdir(), "firebasetest-")); + }); + + after(async () => { + await fs.promises.rmdir(tempDir, { recursive: true }); + }); + + it("should unzip a ui emulator zip file", async () => { + const [uiVersion, uiRemoteUrl] = [ + DownloadDetails.ui.version, + DownloadDetails.ui.opts.remoteUrl, + ]; + + const uiZipPath = path.join(tempDir, `ui-v${uiVersion}.zip`); + + await fs.promises.mkdir(tempDir, { recursive: true }); + if (!(await fs.promises.access(uiZipPath).catch(() => false))) { + await downloadFile(uiRemoteUrl, uiZipPath); + } + + await unzip(uiZipPath, path.join(tempDir, "ui")); + + const files = await fs.promises.readdir(tempDir); + expect(files).to.include("ui"); + + const uiFiles = await fs.promises.readdir(path.join(tempDir, "ui")); + expect(uiFiles).to.include("client"); + expect(uiFiles).to.include("server"); + + const serverFiles = await fs.promises.readdir(path.join(tempDir, "ui", "server")); + expect(serverFiles).to.include("server.mjs"); + }).timeout(10000); + + it("should unzip a pubsub emulator zip file", async () => { + const [pubsubVersion, pubsubRemoteUrl] = [ + DownloadDetails.pubsub.version, + DownloadDetails.pubsub.opts.remoteUrl, + ]; + + const pubsubZipPath = path.join(tempDir, `pubsub-emulator-v${pubsubVersion}.zip`); + + if (!(await fs.promises.access(pubsubZipPath).catch(() => false))) { + await downloadFile(pubsubRemoteUrl, pubsubZipPath); + } + + await unzip(pubsubZipPath, path.join(tempDir, "pubsub")); + + const files = await fs.promises.readdir(tempDir); + expect(files).to.include("pubsub"); + + const pubsubFiles = await fs.promises.readdir(path.join(tempDir, "pubsub")); + expect(pubsubFiles).to.include("pubsub-emulator"); + + const pubsubEmulatorFiles = await fs.promises.readdir( + path.join(tempDir, "pubsub", "pubsub-emulator"), + ); + expect(pubsubEmulatorFiles).to.include("bin"); + expect(pubsubEmulatorFiles).to.include("lib"); + + const binFiles = await fs.promises.readdir( + path.join(tempDir, "pubsub", "pubsub-emulator", "bin"), + ); + expect(binFiles).to.include("cloud-pubsub-emulator"); + }).timeout(10000); +}); + +async function downloadFile(url: string, targetPath: string): Promise { + const u = new URL(url); + const c = new Client({ urlPrefix: u.origin, auth: false }); + + const writeStream = fs.createWriteStream(targetPath); + + const res = await c.request({ + method: "GET", + path: u.pathname, + queryParams: u.searchParams, + responseType: "stream", + resolveOnHTTPError: true, + }); + + if (res.status !== 200) { + throw new Error( + `Download failed, file "${url}" does not exist. status ${ + res.status + }: ${await res.response.text()}`, + { + cause: new Error( + `Object DownloadDetails from src${path.sep}emulator${path.sep}downloadableEmulators.ts contains invalid URL: ${url}`, + ), + }, + ); + } + + return new Promise((resolve, reject) => { + writeStream.on("finish", () => { + resolve(targetPath); + }); + writeStream.on("error", (err) => { + reject(err); + }); + res.body.pipe(writeStream); + }); +} diff --git a/scripts/examples/hosting/update-single-file/README.md b/scripts/examples/hosting/update-single-file/README.md new file mode 100644 index 00000000000..62c4b7e122e --- /dev/null +++ b/scripts/examples/hosting/update-single-file/README.md @@ -0,0 +1,58 @@ +# update-single-file + +This is an example script for how to use `google-auth-library` to upload a single file to a Hosting site. + +## Getting Started + +The easiest way to run the tool is to link it into your Node environment, set up authentication, and run the script in your project folder. + +### Build the Script + +To run this, clone the repository, go to this directory, and build it: + +```bash +cd firebase-tools/scripts/examples/hosting/update-single-file/ +npm install +npm run build +npm link +``` + +### Set up Credentials + +Two options exist to set up credentials. If you're running in a GCP environment (like Cloud Shell), you may be able to skip this step entirely. + +First option, set up application default credentials via `gcloud`: + +```bash +# Set up application default credentials using gcloud (optional if in GCP environment). +gcloud auth application-default login +# It may be required to set a quota project for the credentials - used to account for the API usage. +gcloud auth application-default set-quota-project +``` + +Alternatively, if you (want to) use a service account and set `GOOGLE_APPLICATION_CREDENTIALS` instead of using `gcloud`, that works well too. See Google Cloud's [getting started with authentication](https://cloud.google.com/docs/authentication/getting-started) for more infromation on how to set one up. + +### Run the Script + +In the directory that you specified as `public` in your Firebase Hosting configuration: + +```bash +cd my-app/public/ +update-single-file --project [--site ] +``` + +For example, if you want to update `/team/about.html` in your site you would: + +```bash +cd my-app/public/ +update-single-file --project my-app team/about.html +``` + +## Options + +`--project `: **required** specifies the project deploy to. +`--site `: specifies the site to deploy to, defaults to ``. + +## Debugging + +To see logs of HTTP requests being made, run the script with `DEBUG=update-single-file`. diff --git a/scripts/examples/hosting/update-single-file/package-lock.json b/scripts/examples/hosting/update-single-file/package-lock.json new file mode 100644 index 00000000000..1a7aac3473c --- /dev/null +++ b/scripts/examples/hosting/update-single-file/package-lock.json @@ -0,0 +1,588 @@ +{ + "name": "update-single-file", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "update-single-file", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "google-auth-library": "^8.2.0", + "minimist": "^1.2.6" + }, + "bin": { + "update-single-file": "lib/index.js" + }, + "devDependencies": { + "@types/debug": "^4.1.7", + "typescript": "^4.7.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "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==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "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" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "engines": { + "node": "*" + } + }, + "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==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, + "node_modules/gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-auth-library": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.2.0.tgz", + "integrity": "sha512-wCWs44jLT3cbzONVk2NKK1sfWaKCqzR21cudG3r9tV53/DC/wImxEr7HK5OBlEU5Q1iepZsrdFOxvzmC0M/YRQ==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.0.tgz", + "integrity": "sha512-lRTMn5ElBdDixv4a86bixejPSRk1boRtUowNepeKEVvYiFlkLuAJUVpEz6PfObDHYEKnZWq/9a2zC98xu62A9w==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gtoken": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.1.tgz", + "integrity": "sha512-HPM4VzzPEGxjQ7T2xLrdSYBs+h1c0yHAUiN+8RHPDoiZbndlpg9Sx3SjWcrTt9+N3FHsSABEpjvdQVan5AAuZQ==", + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "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==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "engines": { + "node": ">= 6.13.0" + } + }, + "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" + } + ] + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "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==" + }, + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "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==", + "requires": { + "debug": "4" + } + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + }, + "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==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, + "gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, + "gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.2.0.tgz", + "integrity": "sha512-wCWs44jLT3cbzONVk2NKK1sfWaKCqzR21cudG3r9tV53/DC/wImxEr7HK5OBlEU5Q1iepZsrdFOxvzmC0M/YRQ==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.0.tgz", + "integrity": "sha512-lRTMn5ElBdDixv4a86bixejPSRk1boRtUowNepeKEVvYiFlkLuAJUVpEz6PfObDHYEKnZWq/9a2zC98xu62A9w==", + "requires": { + "node-forge": "^1.3.1" + } + }, + "gtoken": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.1.tgz", + "integrity": "sha512-HPM4VzzPEGxjQ7T2xLrdSYBs+h1c0yHAUiN+8RHPDoiZbndlpg9Sx3SjWcrTt9+N3FHsSABEpjvdQVan5AAuZQ==", + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "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==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "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==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "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==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true + }, + "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==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/scripts/examples/hosting/update-single-file/package.json b/scripts/examples/hosting/update-single-file/package.json new file mode 100644 index 00000000000..d8b04fe454f --- /dev/null +++ b/scripts/examples/hosting/update-single-file/package.json @@ -0,0 +1,28 @@ +{ + "name": "update-single-file", + "version": "0.0.0", + "description": "example of using the Hosting API to update a single file", + "type": "module", + "bin": "./lib/index.js", + "exports": "./lib/index.js", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "prepare": "npm run build", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "engines": { + "node": ">=20.0.0" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@types/debug": "^4.1.7", + "typescript": "^4.7.4" + }, + "dependencies": { + "debug": "^4.3.4", + "google-auth-library": "^8.2.0", + "minimist": "^1.2.6" + } +} diff --git a/scripts/examples/hosting/update-single-file/src/index.ts b/scripts/examples/hosting/update-single-file/src/index.ts new file mode 100644 index 00000000000..07d17af3038 --- /dev/null +++ b/scripts/examples/hosting/update-single-file/src/index.ts @@ -0,0 +1,186 @@ +#!/usr/bin/env node +/** + * Copyright (c) 2022 Google LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import zlib from "node:zlib"; + +import debugPkg from "debug"; +import minimist from "minimist"; +import { GoogleAuth } from "google-auth-library"; + +const debug = debugPkg("update-single-file"); + +const HOSTING_URL = "https://firebasehosting.googleapis.com/v1beta1"; + +async function main(): Promise { + const argv = minimist<{ project?: string; site?: string }>(process.argv.slice(2)); + const PROJECT_ID = argv.project; + if (!PROJECT_ID) { + throw new Error(`--project must be provided.`); + } + const SITE_ID = argv.site || PROJECT_ID; + const files = Array.from(argv._); + console.log(`Deploying files:`); + for (const f of files) { + console.log(`- ${f}`); + } + + const filesByHash: Record = {}; + for (const file of files) { + const hash = await hashFile(file); + filesByHash[hash] = file; + } + + const auth = new GoogleAuth({ + scopes: "https://www.googleapis.com/auth/cloud-platform", + projectId: PROJECT_ID, + }); + const client = await auth.getClient(); + + const res = await client.request<{ release: { name: string; version: { name: string } } }>({ + url: `${HOSTING_URL}/projects/${PROJECT_ID}/sites/${SITE_ID}/channels/live`, + }); + debug("%d %j", res.status, res.data); + + const release = res.data.release.name; + const currentVersion = res.data.release.version.name; + + debug(`Release name: ${release}`); + debug(`Current version name: ${currentVersion}`); + + const exclude: string[] = []; + for (let f of Object.values(filesByHash)) { + f = f.startsWith("/") ? f : `/${f}`; + exclude.push(`^${f.replace("/", "\\/")}$`); + } + debug("Excludes:", exclude); + const cloneRes = await client.request<{ name: string }>({ + method: "POST", + url: `${HOSTING_URL}/projects/${PROJECT_ID}/sites/${SITE_ID}/versions:clone`, + body: JSON.stringify({ + sourceVersion: currentVersion, + finalize: false, + exclude: { regexes: exclude }, + }), + }); + + debug("%d %j", cloneRes.status, cloneRes.data); + const operationName = cloneRes.data.name; + debug(`Operation name: ${operationName}`); + + let done = false; + let newVersion = ""; + while (!done) { + const opRes = await client.request<{ done: boolean; response: { name: string } }>({ + url: `${HOSTING_URL}/${operationName}`, + }); + debug("%d %j", opRes.status, opRes.data); + done = !!opRes.data.done; + newVersion = opRes.data.response?.name; + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } + + debug(`New version: ${newVersion}`); + + const data: Record = {}; + for (let [h, f] of Object.entries(filesByHash)) { + if (!f.startsWith("/")) { + f = `/${f}`; + } + data[f] = h; + } + debug("Posting populate files: %o", { files: data }); + const populateRes = await client.request<{ uploadUrl: string; uploadRequiredHashes?: string[] }>({ + method: "POST", + url: `${HOSTING_URL}/projects/${PROJECT_ID}/${newVersion}:populateFiles`, + body: JSON.stringify({ files: data }), + }); + debug("%d %j", populateRes.status, populateRes.data); + + const uploadURL = populateRes.data.uploadUrl; + const uploadRequiredHashes = populateRes.data.uploadRequiredHashes || []; + if (Array.isArray(uploadRequiredHashes) && uploadRequiredHashes.length) { + for (const h of uploadRequiredHashes) { + const uploadRes = await client.request({ + method: "POST", + url: `${uploadURL}/${h}`, + data: fs.createReadStream(filesByHash[h]).pipe(zlib.createGzip({ level: 9 })), + }); + debug("%d %j", uploadRes.status, uploadRes.data); + if (uploadRes.status !== 200) { + throw new Error(`Failed to upload file ${filesByHash[h]} (${h})`); + } + } + } + + const finalizeRes = await client.request({ + method: "PATCH", + url: `${HOSTING_URL}/projects/${PROJECT_ID}/${newVersion}`, + params: { updateMask: "status" }, + body: JSON.stringify({ + status: "FINALIZED", + }), + }); + debug("%d %j", finalizeRes.status, finalizeRes.data); + + const releaseRes = await client.request({ + method: "POST", + url: `${HOSTING_URL}/projects/${PROJECT_ID}/sites/${SITE_ID}/releases`, + params: { versionName: newVersion }, + body: JSON.stringify({ + message: "Deployed from single file uploader.", + }), + }); + debug("%d %j", releaseRes.status, releaseRes.data); + + const siteRes = await client.request<{ defaultUrl: string }>({ + url: `${HOSTING_URL}/projects/${PROJECT_ID}/sites/${SITE_ID}`, + }); + debug("%d %j", siteRes.status, siteRes.data); + + console.log(`Successfully deployed! Site URL: ${siteRes.data.defaultUrl}`); +} + +async function hashFile(file: string): Promise { + const hasher = crypto.createHash("sha256"); + const gzipper = zlib.createGzip({ level: 9 }); + const gzipStream = fs.createReadStream(path.resolve(process.cwd(), file)).pipe(gzipper); + const p = new Promise((resolve, reject) => { + hasher.once("readable", () => { + debug(`Hashed file ${file}`); + const data = hasher.read() as Buffer | string | undefined; + if (data && typeof data === "string") { + return resolve(data); + } else if (data && Buffer.isBuffer(data)) { + return resolve(data.toString("hex")); + } + reject(new Error(`could not get the hash for file ${file}`)); + }); + gzipStream.once("error", reject); + }); + gzipStream.pipe(hasher); + return p; +} + +void main(); diff --git a/scripts/examples/hosting/update-single-file/tsconfig.json b/scripts/examples/hosting/update-single-file/tsconfig.json new file mode 100644 index 00000000000..fe160da23a8 --- /dev/null +++ b/scripts/examples/hosting/update-single-file/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "es2020", + "strict": true, + "outDir": "lib", + "removeComments": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "target": "es2020" + }, + "include": ["src/**/*"] +} diff --git a/scripts/extensions-deploy-tests/run.sh b/scripts/extensions-deploy-tests/run.sh index a4677b43c29..a02c539c814 100755 --- a/scripts/extensions-deploy-tests/run.sh +++ b/scripts/extensions-deploy-tests/run.sh @@ -2,10 +2,6 @@ set -e # Immediately exit on failure # Globally link the CLI for the testing framework -./scripts/npm-link.sh +./scripts/clean-install.sh -mocha \ - --require ts-node/register \ - --require source-map-support/register \ - --require src/test/helpers/mocha-bootstrap.ts \ - scripts/extensions-deploy-tests/tests.ts +mocha scripts/extensions-deploy-tests/tests.ts diff --git a/scripts/extensions-deploy-tests/tests.ts b/scripts/extensions-deploy-tests/tests.ts index 465de7be7ee..5ed1c53e5a6 100644 --- a/scripts/extensions-deploy-tests/tests.ts +++ b/scripts/extensions-deploy-tests/tests.ts @@ -1,6 +1,4 @@ import { expect } from "chai"; -import * as subprocess from "child_process"; -import { cli } from "winston/lib/winston/config"; import { CLIProcess } from "../integration-helpers/cli"; @@ -32,7 +30,7 @@ describe("firebase deploy --only extensions", () => { if (`${data}`.match(/Deploy complete/)) { return true; } - } + }, ); let output: any; await cli.start("ext:list", FIREBASE_PROJECT, ["--json"], (data: any) => { @@ -46,16 +44,16 @@ describe("firebase deploy --only extensions", () => { (i: any) => i.instanceId === "test-instance1" && i.extension === "firebase/firestore-bigquery-export" && - i.state === "ACTIVE" - ) + i.state === "ACTIVE", + ), ).to.be.true; expect( output.result.some( (i: any) => i.instanceId === "test-instance2" && i.extension === "firebase/storage-resize-images" && - i.state === "ACTIVE" - ) + i.state === "ACTIVE", + ), ).to.be.true; }); }); diff --git a/scripts/extensions-emulator-tests/.firebaserc b/scripts/extensions-emulator-tests/.firebaserc new file mode 100644 index 00000000000..f7b55c6f220 --- /dev/null +++ b/scripts/extensions-emulator-tests/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "fir-tools-testing" + } +} diff --git a/scripts/extensions-emulator-tests/.gitignore b/scripts/extensions-emulator-tests/.gitignore new file mode 100644 index 00000000000..c360d67af3d --- /dev/null +++ b/scripts/extensions-emulator-tests/.gitignore @@ -0,0 +1,73 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +database-debug.log* +firestore-debug.log* +pubsub-debug.log* + +cache/* + +# NPM +package-lock.json + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env diff --git a/scripts/extensions-emulator-tests/extensions/resize-images.env b/scripts/extensions-emulator-tests/extensions/resize-images.env new file mode 100644 index 00000000000..971e82c8ede --- /dev/null +++ b/scripts/extensions-emulator-tests/extensions/resize-images.env @@ -0,0 +1,7 @@ +IMG_SIZES=200x200 +DELETE_ORIGINAL_FILE=false +IMAGE_TYPE=png +LOCATION=us-central1 +IMG_BUCKET=${param:PROJECT_ID}.appspot.com +EVENTARC_CHANNEL=projects/${param:PROJECT_ID}/locations/us-west1/channels/firebase +ALLOWED_EVENT_TYPES=firebase.extensions.storage-resize-images.v1.complete diff --git a/scripts/extensions-emulator-tests/firebase.json b/scripts/extensions-emulator-tests/firebase.json new file mode 100644 index 00000000000..7a8b73cf9b3 --- /dev/null +++ b/scripts/extensions-emulator-tests/firebase.json @@ -0,0 +1,26 @@ +{ + "extensions": { + "resize-images": "firebase/storage-resize-images@0.1.28" + }, + "storage": { + "rules": "storage.rules" + }, + "functions": {}, + "emulators": { + "hub": { + "port": 4000 + }, + "storage": { + "port": 9199 + }, + "functions": { + "port": 9002 + }, + "firestore": { + "port": 8080 + }, + "eventarc": { + "port": 9299 + } + } +} diff --git a/scripts/triggers-end-to-end-tests/functions/.gitignore b/scripts/extensions-emulator-tests/functions/.gitignore similarity index 100% rename from scripts/triggers-end-to-end-tests/functions/.gitignore rename to scripts/extensions-emulator-tests/functions/.gitignore diff --git a/scripts/extensions-emulator-tests/functions/index.js b/scripts/extensions-emulator-tests/functions/index.js new file mode 100644 index 00000000000..1a3f938b739 --- /dev/null +++ b/scripts/extensions-emulator-tests/functions/index.js @@ -0,0 +1,28 @@ +const admin = require("firebase-admin"); +const functions = require("firebase-functions"); +const { onCustomEventPublished } = require("firebase-functions/v2/eventarc"); + +admin.initializeApp(); + +const STORAGE_FILE_NAME = "test.png"; + +exports.writeToDefaultStorage = functions.https.onRequest(async (req, res) => { + await admin.storage().bucket().upload(STORAGE_FILE_NAME); + console.log("Wrote to default Storage bucket"); + res.json({ created: "ok" }); +}); + +exports.eventhandler = onCustomEventPublished( + { + eventType: "firebase.extensions.storage-resize-images.v1.complete", + channel: "locations/us-west1/channels/firebase", + region: "us-west1", + }, + (event) => { + admin + .firestore() + .collection("resizedImages") + .doc(STORAGE_FILE_NAME) + .set({ eventHandlerFired: true }); + }, +); diff --git a/scripts/extensions-emulator-tests/functions/package.json b/scripts/extensions-emulator-tests/functions/package.json new file mode 100644 index 00000000000..65c4052314b --- /dev/null +++ b/scripts/extensions-emulator-tests/functions/package.json @@ -0,0 +1,15 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": {}, + "engines": { + "node": "20" + }, + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0", + "fs-extra": "^5.0.0", + "rimraf": "^3.0.0" + }, + "private": true +} diff --git a/scripts/extensions-emulator-tests/functions/test.png b/scripts/extensions-emulator-tests/functions/test.png new file mode 100644 index 00000000000..7c2400eab51 Binary files /dev/null and b/scripts/extensions-emulator-tests/functions/test.png differ diff --git a/scripts/extensions-emulator-tests/greet-the-world/.gitignore b/scripts/extensions-emulator-tests/greet-the-world/.gitignore deleted file mode 100644 index d8b83df9cdb..00000000000 --- a/scripts/extensions-emulator-tests/greet-the-world/.gitignore +++ /dev/null @@ -1 +0,0 @@ -package-lock.json diff --git a/scripts/extensions-emulator-tests/greet-the-world/extension.yaml b/scripts/extensions-emulator-tests/greet-the-world/extension.yaml deleted file mode 100644 index 988b31339ff..00000000000 --- a/scripts/extensions-emulator-tests/greet-the-world/extension.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# Learn detailed information about the fields of an extension.yaml file in the docs - -name: greet-the-world # Identifier for the extension -specVersion: v1beta # Version of the Firebase Extensions specification -version: 0.0.1 # Follow semver versioning -license: Apache-2.0 # https://spdx.org/licenses/ - -# Friendly display name for your extension (~3-5 words) -displayName: Greet the world - -# Brief description of the task your extension performs (~1 sentence) -description: >- - Sends the world a specified greeting. - -billingRequired: false # Learn more in the docs - -# For your extension to interact with other Google APIs (like Firestore, Cloud Storage, or Cloud Translation), -# set the `apis` field. In addition, set the `roles` field to grant appropriate IAM access to interact with these products. -# Learn about these fields in the docs - -# Learn about the `resources` field in the docs -resources: - - name: greetTheWorld - type: firebaseextensions.v1beta.function - description: >- - HTTPS-triggered function that responds with a specified greeting message - properties: - sourceDirectory: . - location: ${LOCATION} - httpsTrigger: {} - -# Learn about the `params` field in the docs -params: - - param: GREETING - type: string - label: Greeting for the world - description: >- - What do you want to say to the world? For example, Hello world? or What's up, world? - default: Hello - required: true - immutable: false - - - param: LOCATION - type: select - label: Cloud Functions location - description: >- - Where do you want to deploy the functions created for this extension? For help selecting a - location, refer to the [location selection - guide](https://firebase.google.com/docs/functions/locations). - options: - - label: Iowa (us-central1) - value: us-central1 - - label: South Carolina (us-east1) - value: us-east1 - - label: Northern Virginia (us-east4) - value: us-east4 - - label: Belgium (europe-west1) - value: europe-west1 - - label: London (europe-west2) - value: europe-west2 - - label: Hong Kong (asia-east2) - value: asia-east2 - - label: Tokyo (asia-northeast1) - value: asia-northeast1 - default: us-central1 - required: true - immutable: true diff --git a/scripts/extensions-emulator-tests/greet-the-world/functions/index.js b/scripts/extensions-emulator-tests/greet-the-world/functions/index.js deleted file mode 100644 index 6203bf1ad69..00000000000 --- a/scripts/extensions-emulator-tests/greet-the-world/functions/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This template contains a HTTP function that responds with a greeting when called - * - * Always use the FUNCTIONS HANDLER NAMESPACE - * when writing Cloud Functions for extensions. - * Learn more about the handler namespace in the docs - * - * Reference PARAMETERS in your functions code with: - * `process.env.` - * Learn more about parameters in the docs - */ - -const functions = require("firebase-functions"); - -exports.greetTheWorld = functions.handler.https.onRequest((req, res) => { - // Here we reference a user-provided parameter (its value is provided by the user during installation) - const consumerProvidedGreeting = process.env.GREETING; - - // And here we reference an auto-populated parameter (its value is provided by Firebase after installation) - const instanceId = process.env.EXT_INSTANCE_ID; - - const greeting = `${consumerProvidedGreeting} World from ${instanceId}`; - - res.send(greeting); -}); diff --git a/scripts/extensions-emulator-tests/greet-the-world/package.json b/scripts/extensions-emulator-tests/greet-the-world/package.json deleted file mode 100644 index 03f47e0b627..00000000000 --- a/scripts/extensions-emulator-tests/greet-the-world/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "greet-the-world", - "version": "1.0.0", - "description": "", - "main": "functions/index.js", - "dependencies": { - "firebase-admin": "^9.4.2", - "firebase-functions": "^3.15.1" - }, - "author": "", - "license": "MIT" -} diff --git a/scripts/extensions-emulator-tests/greet-the-world/test-firebase.json b/scripts/extensions-emulator-tests/greet-the-world/test-firebase.json deleted file mode 100644 index 92607c48562..00000000000 --- a/scripts/extensions-emulator-tests/greet-the-world/test-firebase.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "functions": {}, - "emulators": { - "hub": { - "port": 4000 - }, - "functions": { - "port": 9002 - } - } -} diff --git a/scripts/extensions-emulator-tests/greet-the-world/test-params.env b/scripts/extensions-emulator-tests/greet-the-world/test-params.env deleted file mode 100644 index 428f8e508ce..00000000000 --- a/scripts/extensions-emulator-tests/greet-the-world/test-params.env +++ /dev/null @@ -1,2 +0,0 @@ -GREETING=Hello -LOCATION=us-east1 diff --git a/scripts/extensions-emulator-tests/run.sh b/scripts/extensions-emulator-tests/run.sh index 7588dd30382..6e6acb50cd1 100755 --- a/scripts/extensions-emulator-tests/run.sh +++ b/scripts/extensions-emulator-tests/run.sh @@ -1,15 +1,11 @@ #!/bin/bash -set -e # Immediately exit on failure -# Globally link the CLI for the testing framework -./scripts/npm-link.sh +source scripts/set-default-credentials.sh +./scripts/clean-install.sh -cd scripts/extensions-emulator-tests/greet-the-world -npm i -cd - # Return to root so that we don't need a relative path for mocha +( + cd scripts/extensions-emulator-tests/functions + npm install +) -mocha \ - --require ts-node/register \ - --require source-map-support/register \ - --require src/test/helpers/mocha-bootstrap.ts \ - scripts/extensions-emulator-tests/tests.ts +mocha scripts/extensions-emulator-tests/tests.ts diff --git a/scripts/extensions-emulator-tests/storage.rules b/scripts/extensions-emulator-tests/storage.rules new file mode 100644 index 00000000000..a7db6961cad --- /dev/null +++ b/scripts/extensions-emulator-tests/storage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if true; + } + } +} diff --git a/scripts/extensions-emulator-tests/tests.ts b/scripts/extensions-emulator-tests/tests.ts old mode 100644 new mode 100755 index c1c5b8783b0..d738d6106eb --- a/scripts/extensions-emulator-tests/tests.ts +++ b/scripts/extensions-emulator-tests/tests.ts @@ -1,62 +1,96 @@ import { expect } from "chai"; +import * as admin from "firebase-admin"; import * as fs from "fs"; +import * as rimraf from "rimraf"; import * as path from "path"; -import * as subprocess from "child_process"; import { FrameworkOptions, TriggerEndToEndTest } from "../integration-helpers/framework"; -const EXTENSION_ROOT = path.dirname(__filename) + "/greet-the-world"; - const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; -const FIREBASE_PROJECT_ZONE = "us-east1"; -const TEST_CONFIG_FILE = "test-firebase.json"; -const TEST_FUNCTION_NAME = "greetTheWorld"; /* * Various delays that are needed because this test spawns * parallel emulator subprocesses. */ -const TEST_SETUP_TIMEOUT = 60000; -const EMULATORS_SHUTDOWN_DELAY_MS = 5000; +const TEST_SETUP_TIMEOUT = 120000; +const EMULATORS_WRITE_DELAY_MS = 5000; +const EMULATORS_SHUTDOWN_DELAY_MS = 25000; +const EMULATOR_TEST_TIMEOUT = EMULATORS_WRITE_DELAY_MS * 2; +const STORAGE_FILE_NAME = "test.png"; +const STORAGE_RESIZED_FILE_NAME = "test_200x200.png"; + +function setUpExtensionsCache(): void { + process.env.FIREBASE_EXTENSIONS_CACHE_PATH = path.join(__dirname, "cache"); + cleanUpExtensionsCache(); + fs.mkdirSync(process.env.FIREBASE_EXTENSIONS_CACHE_PATH); +} + +function cleanUpExtensionsCache(): void { + if ( + process.env.FIREBASE_EXTENSIONS_CACHE_PATH && + fs.existsSync(process.env.FIREBASE_EXTENSIONS_CACHE_PATH) + ) { + rimraf.sync(process.env.FIREBASE_EXTENSIONS_CACHE_PATH); + } +} function readConfig(): FrameworkOptions { - const filename = path.join(EXTENSION_ROOT, "test-firebase.json"); + const filename = path.join(__dirname, "firebase.json"); const data = fs.readFileSync(filename, "utf8"); return JSON.parse(data); } -describe("extension emulator", () => { +describe("CF3 and Extensions emulator", () => { let test: TriggerEndToEndTest; before(async function (this) { this.timeout(TEST_SETUP_TIMEOUT); + setUpExtensionsCache(); expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; - // TODO(joehan): Delete the --open-sesame call when extdev flag is removed. - const p = subprocess.spawnSync("firebase", ["--open-sesame", "extdev"], { cwd: __dirname }); - console.log("open-sesame output:", p.stdout.toString()); - - test = new TriggerEndToEndTest(FIREBASE_PROJECT, EXTENSION_ROOT, readConfig()); - await test.startExtEmulators([ - "--test-params", - "test-params.env", - "--test-config", - TEST_CONFIG_FILE, - ]); + const config = readConfig(); + const storagePort = config.emulators!.storage.port; + process.env.STORAGE_EMULATOR_HOST = `http://127.0.0.1:${storagePort}`; + + const firestorePort = config.emulators!.firestore.port; + process.env.FIRESTORE_EMULATOR_HOST = `localhost:${firestorePort}`; + + test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); + await test.startEmulators(["--only", "functions,extensions,storage,eventarc,firestore"]); + + admin.initializeApp({ + projectId: FIREBASE_PROJECT, + credential: admin.credential.applicationDefault(), + storageBucket: `${FIREBASE_PROJECT}.appspot.com`, + }); }); after(async function (this) { this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + cleanUpExtensionsCache(); await test.stopEmulators(); }); - it("should execute an HTTP function", async function (this) { - this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + it("should call a CF3 HTTPS function to write to the default Storage bucket, then trigger the resize images extension", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - const res = await test.invokeHttpFunction(TEST_FUNCTION_NAME, FIREBASE_PROJECT_ZONE); + const response = await test.writeToDefaultStorage(); + expect(response.status).to.equal(200); - expect(res.status).to.equal(200); - await expect(res.text()).to.eventually.equal("Hello World from greet-the-world"); + /* + * We delay here so that the functions have time to write and trigger - + * this is happening in real time in a different process, so we have to wait like this. + */ + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + const fileResized = await admin.storage().bucket().file(STORAGE_RESIZED_FILE_NAME).exists(); + expect(fileResized[0]).to.be.true; + const eventFired = await admin + .firestore() + .collection("resizedImages") + .doc(STORAGE_FILE_NAME) + .get(); + expect(eventFired.exists).to.be.true; + expect(eventFired.data()?.eventHandlerFired).to.be.true; }); }); diff --git a/scripts/firebase-docker-image/Dockerfile b/scripts/firebase-docker-image/Dockerfile new file mode 100644 index 00000000000..11909309f91 --- /dev/null +++ b/scripts/firebase-docker-image/Dockerfile @@ -0,0 +1,13 @@ +FROM node:lts-alpine AS app-env + +# Install Python and Java and pre-cache emulator dependencies. +RUN apk add --no-cache python3 py3-pip openjdk11-jre bash && \ + npm install -g firebase-tools && \ + firebase setup:emulators:database && \ + firebase setup:emulators:firestore && \ + firebase setup:emulators:pubsub && \ + firebase setup:emulators:storage && \ + firebase setup:emulators:ui && \ + rm -rf /var/cache/apk/* + +ENTRYPOINT [ "firebase" ] \ No newline at end of file diff --git a/scripts/firebase-docker-image/cloudbuild.yaml b/scripts/firebase-docker-image/cloudbuild.yaml new file mode 100644 index 00000000000..ab93d75ced3 --- /dev/null +++ b/scripts/firebase-docker-image/cloudbuild.yaml @@ -0,0 +1,5 @@ +steps: + - name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/$PROJECT_ID/firebase", "."] +images: + - "gcr.io/$PROJECT_ID/firebase" diff --git a/scripts/firebase-docker-image/run.sh b/scripts/firebase-docker-image/run.sh new file mode 100644 index 00000000000..a8c5e178eb1 --- /dev/null +++ b/scripts/firebase-docker-image/run.sh @@ -0,0 +1,4 @@ +PROJECT_ID=joehanley-public +gcloud --project $PROJECT_ID \ + builds \ + submit \ No newline at end of file diff --git a/scripts/firepit-builder/Dockerfile b/scripts/firepit-builder/Dockerfile index f04522d09e1..c35b8dd6aae 100644 --- a/scripts/firepit-builder/Dockerfile +++ b/scripts/firepit-builder/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12 +FROM node:18 # Install dependencies RUN apt-get update && \ @@ -8,6 +8,9 @@ RUN apt-get update && \ RUN curl -fsSL --output hub.tgz https://github.com/github/hub/releases/download/v2.11.2/hub-linux-amd64-2.11.2.tgz RUN tar --strip-components=2 -C /usr/bin -xf hub.tgz hub-linux-amd64-2.11.2/bin/hub +# Upgrade npm to 9. +RUN npm install --global npm@9.5 + # Create app directory WORKDIR /usr/src/app diff --git a/scripts/firepit-builder/cloudbuild.yaml b/scripts/firepit-builder/cloudbuild.yaml new file mode 100644 index 00000000000..2a516860ad3 --- /dev/null +++ b/scripts/firepit-builder/cloudbuild.yaml @@ -0,0 +1,4 @@ +steps: + - name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/$PROJECT_ID/firepit-builder", "."] +images: ["gcr.io/$PROJECT_ID/firepit-builder"] diff --git a/scripts/firepit-builder/package-lock.json b/scripts/firepit-builder/package-lock.json index f1858f6d1c1..53a99c3bdf8 100644 --- a/scripts/firepit-builder/package-lock.json +++ b/scripts/firepit-builder/package-lock.json @@ -1,13 +1,438 @@ { "name": "cloud_build", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "cloud_build", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "shelljs": "^0.8.5", + "yargs": "^13.3.0" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", + "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "dependencies": { + "is-core-module": "^2.8.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" }, "ansi-styles": { "version": "3.2.1", @@ -18,9 +443,9 @@ } }, "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "brace-expansion": { "version": "1.1.11", @@ -87,15 +512,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, "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==" }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -105,6 +535,14 @@ "path-is-absolute": "^1.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -120,9 +558,17 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "requires": { + "has": "^1.0.3" + } }, "is-fullwidth-code-point": { "version": "2.0.0", @@ -139,9 +585,9 @@ } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } @@ -209,11 +655,13 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", + "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", "requires": { - "path-parse": "^1.0.6" + "is-core-module": "^2.8.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "set-blocking": { @@ -222,9 +670,9 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", "requires": { "glob": "^7.0.0", "interpret": "^1.0.0", @@ -249,6 +697,11 @@ "ansi-regex": "^4.1.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", diff --git a/scripts/firepit-builder/package.json b/scripts/firepit-builder/package.json index 6bfba1b5306..3c6d77cc8a7 100644 --- a/scripts/firepit-builder/package.json +++ b/scripts/firepit-builder/package.json @@ -11,7 +11,7 @@ "license": "MIT", "private": true, "dependencies": { - "shelljs": "^0.8.3", + "shelljs": "^0.8.5", "yargs": "^13.3.0" } } diff --git a/scripts/firepit-builder/pipeline.js b/scripts/firepit-builder/pipeline.js index b29e8e4ae23..8c36eb20a42 100755 --- a/scripts/firepit-builder/pipeline.js +++ b/scripts/firepit-builder/pipeline.js @@ -42,7 +42,7 @@ if (fs.existsSync(firebaseToolsPackage)) { npm("install", packedModule); rm(packedModule); } else { - npm("install", firebaseToolsPackage); + npm("install", "--omit=dev", firebaseToolsPackage); } const packageJson = JSON.parse(cat("node_modules/firebase-tools/package.json")); @@ -133,7 +133,7 @@ cd(outputDir); console.log( ls(".") .map((fn) => path.join(pwd().toString(), fn.toString())) - .join("\n") + .join("\n"), ); // Cleanup diff --git a/scripts/functions-deploy-tests/.firebaserc b/scripts/functions-deploy-tests/.firebaserc new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/scripts/functions-deploy-tests/.firebaserc @@ -0,0 +1 @@ +{} diff --git a/scripts/functions-deploy-tests/README.md b/scripts/functions-deploy-tests/README.md new file mode 100644 index 00000000000..c23bd5519a3 --- /dev/null +++ b/scripts/functions-deploy-tests/README.md @@ -0,0 +1,19 @@ +# Function Deploy Integration Test + +Function deploy integration test cycles through "create -> update -> update -> ..." phases to make sure all supported function triggers are deployed with correct configuration values. + +The test isn't "thread-safe" - there should be at most one test running on a project at any given time. I suggest you to use your own project to run the test. + +You can set the test project and run the integration test as follows: + +```bash +$ GCLOUD_PROJECT=${PROJECT_ID} npm run test:functions-deploy +``` + +The integration test blows away all existing functions! Don't run it on a project where you have functions you'd like to keep. + +You can also run the test target with `FIREBASE_DEBUG=true` to pass `--debug` flag to CLI invocation: + +```bash +$ GCLOUD_PROJECT=${PROJECT_ID} FIREBASE_DEBUG=true npm run test:functions-deploy +``` diff --git a/scripts/functions-deploy-tests/cli.ts b/scripts/functions-deploy-tests/cli.ts new file mode 100644 index 00000000000..8a34f229e82 --- /dev/null +++ b/scripts/functions-deploy-tests/cli.ts @@ -0,0 +1,65 @@ +import * as spawn from "cross-spawn"; +import { ChildProcess } from "child_process"; + +// NOTE: This code duplicates scripts/integration-helpers/cli.ts. +// There are minor differences in handling stdout/stderr that triggered forking of the code, +// but in an ideal world, we would have one, more feature-ful library for invoking CLI during tests. +// Blame taeold@ for taking this shortcut. + +export interface Result { + proc: ChildProcess; + stdout: string; + stderr: string; +} + +/** + * Execute a Firebase CLI command. + */ +export function exec( + cmd: string, + project: string, + additionalArgs: string[], + cwd: string, + quiet = true, + extraEnv: Record = {}, +): Promise { + const args = [cmd, "--project", project]; + + if (additionalArgs) { + args.push(...additionalArgs); + } + const env = { + ...process.env, + ...extraEnv, + }; + const proc = spawn("firebase", args, { cwd, env }); + if (!proc) { + throw new Error("Failed to start firebase CLI"); + } + + const cli: Result = { + proc, + stdout: "", + stderr: "", + }; + + proc.stdout?.on("data", (data) => { + const s = data.toString(); + if (!quiet) { + console.log(s); + } + cli.stdout += s; + }); + + proc.stderr?.on("data", (data) => { + const s = data.toString(); + if (!quiet) { + console.log(s); + } + cli.stderr += s; + }); + + return new Promise((resolve) => { + proc.on("exit", () => resolve(cli)); + }); +} diff --git a/scripts/functions-deploy-tests/firebase.json b/scripts/functions-deploy-tests/firebase.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/scripts/functions-deploy-tests/firebase.json @@ -0,0 +1 @@ +{} diff --git a/scripts/functions-deploy-tests/functions/fns.js b/scripts/functions-deploy-tests/functions/fns.js new file mode 100644 index 00000000000..d5c746331ca --- /dev/null +++ b/scripts/functions-deploy-tests/functions/fns.js @@ -0,0 +1,47 @@ +import * as v1 from "firebase-functions"; +import * as v2 from "firebase-functions/v2"; +import { v1Opts, v2Opts, v1ScheduleOpts, v2ScheduleOpts, v1TqOpts, v2TqOpts } from "./options.js"; + +// v1 functions +const withOptions = v1.runWith(v1Opts); +export const v1db = withOptions.database.ref("/foo/bar").onWrite(() => {}); +export const v1fire = withOptions.firestore.document("foo/bar").onWrite(() => {}); +export const v1auth = withOptions.auth.user().onCreate(() => {}); +export const v1pubsub = withOptions.pubsub.topic("foo").onPublish(() => {}); +export const v1scheduled = withOptions.pubsub + .schedule("every 30 minutes") + .retryConfig(v1ScheduleOpts) + .onRun(() => {}); +export const v1an = withOptions.analytics.event("in_app_purchase").onLog(() => {}); +export const v1rc = withOptions.remoteConfig.onUpdate(() => {}); +export const v1storage = withOptions.storage.object().onFinalize(() => {}); +export const v1testlab = withOptions.testLab.testMatrix().onComplete(() => {}); +export const v1tq = withOptions.tasks.taskQueue(v1TqOpts).onDispatch(() => {}); +// TODO: Deploying IdP fns fail because we can't make public functions in google.com GCP projects. +// export const v1idp = withOptions.auth.user(v1IdpOpts).beforeCreate(() => {}); +// TODO: Deploying https fn fails because we can't make public functions in google.com GCP projecs. +// export const v1req = withOptions.https.onRequest(() => {}); +// export const v1callable = withOptions.https.onCall(() => {}); +export const v1secret = v1 + .runWith({ ...v1Opts, secrets: ["TOP"] }) + .pubsub.topic("foo") + .onPublish(() => {}); + +// v2 functions +v2.setGlobalOptions(v2Opts); +export const v2storage = v2.storage.onObjectFinalized(() => {}); +export const v2pubsub = v2.pubsub.onMessagePublished("foo", () => {}); +export const v2alerts = v2.alerts.billing.onPlanAutomatedUpdatePublished({}, () => {}); +export const v2tq = v2.tasks.onTaskDispatched(v2TqOpts, () => {}); +// TODO: Deploying IdP fns fail because we can't make public functions in google.com GCP projects. +// export const v2idp = v2.identity.beforeUserSignedIn(v2IdpOpts, () => {}); +// TODO: Deploying https fn fails because we can't make public functions in google.com GCP projecs. +// export const v2req = v2.https.onRequest(() => {}); +// export const v2call = v2.https.onCall(() => {}); +// TODO: Need a way to create default firebase custom channel as part of integration test. +// export const v2custom = v2.eventarc.onCustomEventPublished("custom.event", () => {}); +export const v2secret = v2.pubsub.onMessagePublished({ topic: "foo", secrets: ["TOP"] }, () => {}); +export const v2scheduled = v2.scheduler.onSchedule(v2ScheduleOpts, () => {}); +export const v2testlab = v2.testLab.onTestMatrixCompleted(() => {}); +export const v2rc = v2.remoteConfig.onConfigUpdated(() => {}); +export const v2perf = v2.alerts.performance.onThresholdAlertPublished(() => {}); diff --git a/scripts/functions-deploy-tests/functions/package.json b/scripts/functions-deploy-tests/functions/package.json new file mode 100644 index 00000000000..6c010cae45f --- /dev/null +++ b/scripts/functions-deploy-tests/functions/package.json @@ -0,0 +1,17 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "firebase-admin": "^11.0.0", + "firebase-functions": "^4.1.0" + }, + "engines": { + "node": "20" + }, + "private": true +} diff --git a/scripts/functions-deploy-tests/run.sh b/scripts/functions-deploy-tests/run.sh new file mode 100755 index 00000000000..57bacc6ab10 --- /dev/null +++ b/scripts/functions-deploy-tests/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e # Immediately exit on failure + +# Globally link the CLI for the testing framework +./scripts/clean-install.sh + +# Create a secret for testing if it doesn't exist +firebase functions:secrets:get TOP --project $GCLOUD_PROJECT || (echo secret | firebase functions:secrets:set --data-file=- TOP --project $GCLOUD_PROJECT -f) + +(cd scripts/functions-deploy-tests/functions && npm i) + +mocha scripts/functions-deploy-tests/tests.ts \ No newline at end of file diff --git a/scripts/functions-deploy-tests/tests.ts b/scripts/functions-deploy-tests/tests.ts new file mode 100644 index 00000000000..2e40949fd26 --- /dev/null +++ b/scripts/functions-deploy-tests/tests.ts @@ -0,0 +1,410 @@ +import * as path from "node:path"; +import * as fs from "fs-extra"; + +import { expect } from "chai"; +import * as functions from "firebase-functions"; +import * as functionsv2 from "firebase-functions/v2"; + +import * as cli from "./cli"; +import * as proto from "../../src/gcp/proto"; +import * as tasks from "../../src/gcp/cloudtasks"; +import * as scheduler from "../../src/gcp/cloudscheduler"; +import { Endpoint } from "../../src/deploy/functions/backend"; +import { requireAuth } from "../../src/requireAuth"; + +const FIREBASE_PROJECT = process.env.GCLOUD_PROJECT || ""; +const FIREBASE_DEBUG = process.env.FIREBASE_DEBUG || ""; +const FUNCTIONS_DIR = path.join(__dirname, "functions"); +const FNS_COUNT = 20; + +function genRandomId(n = 10): string { + const charset = "abcdefghijklmnopqrstuvwxyz"; + let id = ""; + for (let i = 0; i < n; i++) { + id += charset.charAt(Math.floor(Math.random() * charset.length)); + } + return id; +} + +interface Opts { + v1Opts: functions.RuntimeOptions; + v2Opts: functionsv2.GlobalOptions; + + v1TqOpts: functions.tasks.TaskQueueOptions; + v2TqOpts: functionsv2.tasks.TaskQueueOptions; + + v1IdpOpts: functions.auth.UserOptions; + v2IdpOpts: functionsv2.identity.BlockingOptions; + + v1ScheduleOpts: functions.ScheduleRetryConfig; + v2ScheduleOpts: functionsv2.scheduler.ScheduleOptions; +} + +async function setOpts(opts: Opts) { + let stmt = ""; + for (const [name, opt] of Object.entries(opts)) { + if (opt) { + stmt += `export const ${name} = ${JSON.stringify(opt)};\n`; + } + } + await fs.writeFile(path.join(FUNCTIONS_DIR, "options.js"), stmt); +} + +async function listFns(runId: string): Promise> { + const result = await cli.exec("functions:list", FIREBASE_PROJECT, ["--json"], __dirname, false); + const output = JSON.parse(result.stdout); + + const eps: Record = {}; + for (const ep of output.result as Endpoint[]) { + const id = ep.id.replace(`${runId}-`, ""); + if (ep.id !== id) { + // By default, functions list does not attempt to fully hydrate configuration options for task queue and schedule + // functions because they require extra API calls. Manually inject details. + if ("taskQueueTrigger" in ep) { + const queue = await tasks.getQueue(tasks.queueNameForEndpoint(ep)); + ep.taskQueueTrigger = tasks.triggerFromQueue(queue); + } + if ("scheduleTrigger" in ep) { + const jobName = scheduler.jobNameForEndpoint(ep, "us-central1"); + const job = (await scheduler.getJob(jobName)).body as scheduler.Job; + if (job.retryConfig) { + const cfg = job.retryConfig; + ep.scheduleTrigger.retryConfig = { + retryCount: cfg.retryCount, + maxDoublings: cfg.maxDoublings, + }; + if (cfg.maxBackoffDuration) { + ep.scheduleTrigger.retryConfig.maxBackoffSeconds = proto.secondsFromDuration( + cfg.maxBackoffDuration, + ); + } + if (cfg.maxRetryDuration) { + ep.scheduleTrigger.retryConfig.maxRetrySeconds = proto.secondsFromDuration( + cfg.maxRetryDuration, + ); + } + if (cfg.minBackoffDuration) { + ep.scheduleTrigger.retryConfig.minBackoffSeconds = proto.secondsFromDuration( + cfg.minBackoffDuration, + ); + } + } + } + + eps[id] = ep; + } + // Ignore functions w/o matching RUN_ID as prefix. + // They are probably left over from previous test runs. + } + return eps; +} + +describe("firebase deploy", function (this) { + this.timeout(1000_000); + + const RUN_ID = genRandomId(); + console.log(`TEST RUN: ${RUN_ID}`); + + async function setOptsAndDeploy(opts: Opts): Promise { + await setOpts(opts); + const args = ["--only", "functions", "--non-interactive", "--force"]; + if (FIREBASE_DEBUG) { + args.push("--debug"); + } + return await cli.exec("deploy", FIREBASE_PROJECT, args, __dirname, false); + } + + before(async () => { + expect(FIREBASE_PROJECT).to.not.be.empty; + + await requireAuth({}); + // write up index.js to import trigger definition using unique group identifier. + // All exported functions will have name {hash}-{trigger} e.g. 'abcdefg-v1storage'. + await fs.writeFile( + path.join(FUNCTIONS_DIR, "index.js"), + `export * as ${RUN_ID} from "./fns.js";`, + ); + }); + + after(async () => { + try { + await fs.unlink(path.join(FUNCTIONS_DIR, "index.js")); + } catch (e: any) { + if (e?.code === "ENOENT") { + return; + } + throw e; + } + }); + + it("deploys functions with runtime options", async () => { + const opts: Opts = { + v1Opts: { + memory: "128MB", + maxInstances: 42, + timeoutSeconds: 42, + preserveExternalChanges: true, + }, + v2Opts: { + memory: "128MiB", + maxInstances: 42, + timeoutSeconds: 42, + cpu: 2, + concurrency: 42, + preserveExternalChanges: true, + }, + v1TqOpts: { + retryConfig: { + maxAttempts: 42, + maxRetrySeconds: 42, + maxBackoffSeconds: 42, + maxDoublings: 42, + minBackoffSeconds: 42, + }, + rateLimits: { + maxDispatchesPerSecond: 42, + maxConcurrentDispatches: 42, + }, + }, + v2TqOpts: { + retryConfig: { + maxAttempts: 42, + maxRetrySeconds: 42, + maxBackoffSeconds: 42, + maxDoublings: 42, + minBackoffSeconds: 42, + }, + rateLimits: { + maxDispatchesPerSecond: 42, + maxConcurrentDispatches: 42, + }, + }, + v1IdpOpts: { + blockingOptions: { + idToken: true, + refreshToken: true, + accessToken: false, + }, + }, + v2IdpOpts: { + idToken: true, + refreshToken: true, + accessToken: true, + }, + v1ScheduleOpts: { + retryCount: 3, + minBackoffDuration: "42s", + maxRetryDuration: "42s", + maxDoublings: 42, + maxBackoffDuration: "42s", + }, + v2ScheduleOpts: { + schedule: "every 30 minutes", + retryCount: 3, + minBackoffSeconds: 42, + maxRetrySeconds: 42, + maxDoublings: 42, + maxBackoffSeconds: 42, + }, + }; + + const result = await setOptsAndDeploy(opts); + expect(result.stdout, "deploy result").to.match(/Deploy complete!/); + + const endpoints = await listFns(RUN_ID); + expect(Object.keys(endpoints).length, "number of deployed functions").to.equal(FNS_COUNT); + + for (const e of Object.values(endpoints)) { + expect(e).to.include({ + availableMemoryMb: 128, + timeoutSeconds: 42, + maxInstances: 42, + }); + if (e.platform === "gcfv2") { + expect(e).to.include({ + cpu: 2, + concurrency: 42, + }); + } + if ("taskQueueTrigger" in e) { + expect(e.taskQueueTrigger).to.deep.equal({ + retryConfig: { + maxAttempts: 42, + maxRetrySeconds: 42, + maxBackoffSeconds: 42, + maxDoublings: 42, + minBackoffSeconds: 42, + }, + rateLimits: { + maxDispatchesPerSecond: 42, + maxConcurrentDispatches: 42, + }, + }); + } + if ("scheduleTrigger" in e) { + expect(e.scheduleTrigger).to.deep.equal({ + retryConfig: { + retryCount: 3, + maxRetrySeconds: 42, + maxBackoffSeconds: 42, + maxDoublings: 42, + minBackoffSeconds: 42, + }, + }); + } + if (e.secretEnvironmentVariables) { + expect(e.secretEnvironmentVariables).to.have.length(1); + expect(e.secretEnvironmentVariables[0]).to.include({ + key: "TOP", + secret: "TOP", + }); + } + } + }); + + it("skips duplicate deploys functions with runtime options when preserveExternalChanges is set", async () => { + const opts: Opts = { + v1Opts: { preserveExternalChanges: true }, + v2Opts: { preserveExternalChanges: true }, + v1TqOpts: {}, + v2TqOpts: {}, + v1IdpOpts: {}, + v2IdpOpts: {}, + v1ScheduleOpts: {}, + v2ScheduleOpts: { schedule: "every 30 minutes" }, + }; + + const result = await setOptsAndDeploy(opts); + expect(result.stdout, "deploy result").to.match(/Deploy complete!/); + + const result2 = await setOptsAndDeploy(opts); + expect(result2.stdout, "deploy result").to.match(/Skipped \(No changes detected\)/); + }); + + it("leaves existing options when unspecified and preserveExternalChanges is set", async () => { + const opts: Opts = { + v1Opts: { preserveExternalChanges: true }, + v2Opts: { preserveExternalChanges: true }, + v1TqOpts: {}, + v2TqOpts: {}, + v1IdpOpts: {}, + v2IdpOpts: {}, + v1ScheduleOpts: {}, + v2ScheduleOpts: { schedule: "every 30 minutes" }, + }; + + const result = await setOptsAndDeploy(opts); + expect(result.stdout, "deploy result").to.match(/Deploy complete!/); + + const endpoints = await listFns(RUN_ID); + expect(Object.keys(endpoints).length, "number of deployed functions").to.equal(FNS_COUNT); + + for (const e of Object.values(endpoints)) { + expect(e).to.include({ + availableMemoryMb: 128, + timeoutSeconds: 42, + maxInstances: 42, + }); + if (e.platform === "gcfv2") { + expect(e).to.include({ + cpu: 2, + // EXCEPTION: concurrency + // Firebase will aggressively set concurrency to 80 when the CPU setting allows for it + // AND when the concurrency is NOT set on the source code. + concurrency: 80, + }); + } + // BUGBUG: As implemented, Cloud Tasks update doesn't preserve existing setting. Instead, it overwrites the + // existing setting with default settings. + // if ("taskQueueTrigger" in e) { + // expect(e.taskQueueTrigger).to.deep.equal({ + // retryConfig: { + // maxAttempts: 42, + // maxRetrySeconds: 42, + // maxBackoffSeconds: 42, + // maxDoublings: 42, + // minBackoffSeconds: 42, + // }, + // rateLimits: { + // maxDispatchesPerSecond: 42, + // maxConcurrentDispatches: 42, + // }, + // }); + // } + if ("scheduleTrigger" in e) { + expect(e.scheduleTrigger).to.deep.equal({ + retryConfig: { + retryCount: 3, + maxRetrySeconds: 42, + maxBackoffSeconds: 42, + maxDoublings: 42, + minBackoffSeconds: 42, + }, + }); + } + if (e.secretEnvironmentVariables) { + expect(e.secretEnvironmentVariables).to.have.length(1); + expect(e.secretEnvironmentVariables[0]).to.include({ + key: "TOP", + secret: "TOP", + }); + } + } + }); + + // BUGBUG: Setting options to null SHOULD restore their values to default, but this isn't correctly implemented in + // the CLI. + it.skip("restores default values when unspecified and preserveExternalChanges is not set", async () => { + const opts: Opts = { + v1Opts: {}, + v2Opts: {}, + v1TqOpts: {}, + v2TqOpts: {}, + v1IdpOpts: { blockingOptions: {} }, + v2IdpOpts: {}, + v1ScheduleOpts: {}, + v2ScheduleOpts: { schedule: "every 30 minutes" }, + }; + + const result = await setOptsAndDeploy(opts); + expect(result.stdout, "deploy result").to.match(/Deploy complete!/); + + const endpoints = await listFns(RUN_ID); + expect(Object.keys(endpoints).length, "number of deployed functions").to.equal(FNS_COUNT); + + for (const e of Object.values(endpoints)) { + expect(e).to.include({ + availableMemoryMb: 128, + timeoutSeconds: 60, + maxInstances: 0, + }); + if (e.platform === "gcfv2") { + expect(e).to.include({ + cpu: 1, + concurrency: 80, + }); + } + if ("taskQueueTrigger" in e) { + expect(e.taskQueueTrigger).to.deep.equal(tasks.DEFAULT_SETTINGS); + } + if ("scheduleTrigger" in e) { + expect(e.scheduleTrigger).to.deep.equal({ + retryConfig: { + retryCount: 3, + maxRetrySeconds: 42, + maxBackoffSeconds: 42, + maxDoublings: 42, + minBackoffSeconds: 42, + }, + }); + } + if (e.secretEnvironmentVariables) { + expect(e.secretEnvironmentVariables).to.have.length(1); + expect(e.secretEnvironmentVariables[0]).to.include({ + key: "TOP", + secret: "TOP", + }); + } + } + }); +}); diff --git a/scripts/functions-discover-tests/fixtures/bundled/dist/index.js b/scripts/functions-discover-tests/fixtures/bundled/dist/index.js new file mode 100644 index 00000000000..827530646c8 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/dist/index.js @@ -0,0 +1,20 @@ +/** + * Minimal example of a "bundled" function source. + * + * Instead of actually bundling the source code, we manually annotate + * the exported function with the __endpoint property to test the situation + * where the distributed package doesn't include Firebase Functions SDK as a + * dependency. + */ + +const hello = (req, resp) => { + resp.send("hello"); +}; + +hello.__endpoint = { + platform: "gcfv2", + region: "region", + httpsTrigger: {}, +}; + +exports.hello = hello; diff --git a/scripts/functions-discover-tests/fixtures/bundled/dist/package.json b/scripts/functions-discover-tests/fixtures/bundled/dist/package.json new file mode 100644 index 00000000000..e356900fb54 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/dist/package.json @@ -0,0 +1,7 @@ +{ + "name": "dist", + "version": "0.0.1", + "engines": { + "node": "20" + } +} diff --git a/scripts/functions-discover-tests/fixtures/bundled/firebase.json b/scripts/functions-discover-tests/fixtures/bundled/firebase.json new file mode 100644 index 00000000000..c9df875f3e4 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/firebase.json @@ -0,0 +1,5 @@ +{ + "functions": { + "source": "dist" + } +} \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/bundled/install.sh b/scripts/functions-discover-tests/fixtures/bundled/install.sh new file mode 100755 index 00000000000..de6890dcdf0 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +npm i diff --git a/scripts/functions-discover-tests/fixtures/bundled/package.json b/scripts/functions-discover-tests/fixtures/bundled/package.json new file mode 100644 index 00000000000..88420ade881 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/package.json @@ -0,0 +1,10 @@ +{ + "name": "dist", + "version": "0.0.1", + "dependencies": { + "firebase-functions": "^4.1.1" + }, + "engines": { + "node": "20" + } +} \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/codebases/firebase.json b/scripts/functions-discover-tests/fixtures/codebases/firebase.json new file mode 100644 index 00000000000..14ded1f8d5a --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/firebase.json @@ -0,0 +1,26 @@ +{ + "functions": [ + { + "source": "v1", + "codebase": "v1", + "runtime": "nodejs16", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log" + ] + }, + { + "source": "v2", + "codebase": "v2", + "runtime": "nodejs16", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log" + ] + } + ] +} \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/codebases/install.sh b/scripts/functions-discover-tests/fixtures/codebases/install.sh new file mode 100755 index 00000000000..b15011a2b4d --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/install.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +(cd v1 && npm i) +(cd v2 && npm i) diff --git a/scripts/functions-discover-tests/fixtures/codebases/v1/index.js b/scripts/functions-discover-tests/fixtures/codebases/v1/index.js new file mode 100644 index 00000000000..f5d2f549a3c --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/v1/index.js @@ -0,0 +1,5 @@ +const functions = require("firebase-functions"); + +exports.hellov1 = functions.https.onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); diff --git a/scripts/functions-discover-tests/fixtures/codebases/v1/package.json b/scripts/functions-discover-tests/fixtures/codebases/v1/package.json new file mode 100644 index 00000000000..8bd6863b7cb --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/v1/package.json @@ -0,0 +1,9 @@ +{ + "name": "codebase-v1", + "dependencies": { + "firebase-functions": "^4.0.0" + }, + "engines": { + "node": "20" + } +} diff --git a/scripts/functions-discover-tests/fixtures/codebases/v2/index.js b/scripts/functions-discover-tests/fixtures/codebases/v2/index.js new file mode 100644 index 00000000000..99f5c72ba74 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/v2/index.js @@ -0,0 +1,5 @@ +import { onRequest } from "firebase-functions/v2/https"; + +export const hellov2 = onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); diff --git a/scripts/functions-discover-tests/fixtures/codebases/v2/package.json b/scripts/functions-discover-tests/fixtures/codebases/v2/package.json new file mode 100644 index 00000000000..732e1665c3c --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/v2/package.json @@ -0,0 +1,10 @@ +{ + "name": "codebase-v2", + "type": "module", + "dependencies": { + "firebase-functions": "^4.0.0" + }, + "engines": { + "node": "20" + } +} diff --git a/scripts/functions-discover-tests/fixtures/esm/firebase.json b/scripts/functions-discover-tests/fixtures/esm/firebase.json new file mode 100644 index 00000000000..93f83c8cfa7 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/esm/firebase.json @@ -0,0 +1,3 @@ +{ + "functions": {} +} diff --git a/scripts/functions-discover-tests/fixtures/esm/functions/index.js b/scripts/functions-discover-tests/fixtures/esm/functions/index.js new file mode 100644 index 00000000000..e4e1f5e78de --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/esm/functions/index.js @@ -0,0 +1,11 @@ +import * as functions from "firebase-functions"; +import { onRequest } from "firebase-functions/v2/https"; + +export const hellov1 = functions.https.onRequest((request, response) => { + functions.logger.info("Hello logs!", { structuredData: true }); + response.send("Hello from Firebase!"); +}); + +export const hellov2 = onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); diff --git a/scripts/functions-discover-tests/fixtures/esm/functions/package.json b/scripts/functions-discover-tests/fixtures/esm/functions/package.json new file mode 100644 index 00000000000..4073fb49d90 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/esm/functions/package.json @@ -0,0 +1,10 @@ +{ + "name": "esm", + "type": "module", + "dependencies": { + "firebase-functions": "^4.0.0" + }, + "engines": { + "node": "20" + } +} diff --git a/scripts/functions-discover-tests/fixtures/esm/install.sh b/scripts/functions-discover-tests/fixtures/esm/install.sh new file mode 100755 index 00000000000..21c35208cf2 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/esm/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +cd functions && npm i diff --git a/scripts/functions-discover-tests/fixtures/pnpm/firebase.json b/scripts/functions-discover-tests/fixtures/pnpm/firebase.json new file mode 100644 index 00000000000..93f83c8cfa7 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/pnpm/firebase.json @@ -0,0 +1,3 @@ +{ + "functions": {} +} diff --git a/scripts/functions-discover-tests/fixtures/pnpm/functions/index.js b/scripts/functions-discover-tests/fixtures/pnpm/functions/index.js new file mode 100644 index 00000000000..cf0342ca53a --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/pnpm/functions/index.js @@ -0,0 +1,10 @@ +const functions = require("firebase-functions"); +const { onRequest } = require("firebase-functions/v2/https"); + +exports.hellov1 = functions.https.onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); + +exports.hellov2 = onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); diff --git a/scripts/functions-discover-tests/fixtures/pnpm/functions/package.json b/scripts/functions-discover-tests/fixtures/pnpm/functions/package.json new file mode 100644 index 00000000000..bc017fc70e5 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/pnpm/functions/package.json @@ -0,0 +1,9 @@ +{ + "name": "pnpm", + "dependencies": { + "firebase-functions": "^4.0.0" + }, + "engines": { + "node": "20" + } +} diff --git a/scripts/functions-discover-tests/fixtures/pnpm/install.sh b/scripts/functions-discover-tests/fixtures/pnpm/install.sh new file mode 100755 index 00000000000..f9e13353e1e --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/pnpm/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +cd functions && pnpm install diff --git a/scripts/functions-discover-tests/fixtures/simple/firebase.json b/scripts/functions-discover-tests/fixtures/simple/firebase.json new file mode 100644 index 00000000000..93f83c8cfa7 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/simple/firebase.json @@ -0,0 +1,3 @@ +{ + "functions": {} +} diff --git a/scripts/functions-discover-tests/fixtures/simple/functions/index.js b/scripts/functions-discover-tests/fixtures/simple/functions/index.js new file mode 100644 index 00000000000..cf0342ca53a --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/simple/functions/index.js @@ -0,0 +1,10 @@ +const functions = require("firebase-functions"); +const { onRequest } = require("firebase-functions/v2/https"); + +exports.hellov1 = functions.https.onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); + +exports.hellov2 = onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); diff --git a/scripts/functions-discover-tests/fixtures/simple/functions/package.json b/scripts/functions-discover-tests/fixtures/simple/functions/package.json new file mode 100644 index 00000000000..1a25153f9e9 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/simple/functions/package.json @@ -0,0 +1,9 @@ +{ + "name": "simple", + "dependencies": { + "firebase-functions": "^4.0.0" + }, + "engines": { + "node": "20" + } +} diff --git a/scripts/functions-discover-tests/fixtures/simple/install.sh b/scripts/functions-discover-tests/fixtures/simple/install.sh new file mode 100755 index 00000000000..21c35208cf2 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/simple/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +cd functions && npm i diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/firebase.json b/scripts/functions-discover-tests/fixtures/yarn-workspaces/firebase.json new file mode 100644 index 00000000000..8189bc54b80 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/firebase.json @@ -0,0 +1,5 @@ +{ + "functions": { + "source": "packages/functions" + } +} \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/install.sh b/scripts/functions-discover-tests/fixtures/yarn-workspaces/install.sh new file mode 100755 index 00000000000..9e1c5a2ab0d --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +yarn install \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/package.json b/scripts/functions-discover-tests/fixtures/yarn-workspaces/package.json new file mode 100644 index 00000000000..643e18c292f --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/package.json @@ -0,0 +1,5 @@ +{ + "name": "yarn-workspace", + "private": true, + "workspaces": ["packages/functions", "packages/a-test-pkg"] +} \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/a-test-pkg/index.js b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/a-test-pkg/index.js new file mode 100644 index 00000000000..f0dc690cd82 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/a-test-pkg/index.js @@ -0,0 +1 @@ +exports.msg = "Hello world!"; diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/a-test-pkg/package.json b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/a-test-pkg/package.json new file mode 100644 index 00000000000..14739ac24c8 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/a-test-pkg/package.json @@ -0,0 +1,5 @@ +{ + "name": "@firebase/a-test-pkg", + "version": "0.0.1", + "private": true +} diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/functions/index.js b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/functions/index.js new file mode 100644 index 00000000000..35bc8c0f570 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/functions/index.js @@ -0,0 +1,11 @@ +const functions = require("firebase-functions"); +const { onRequest } = require("firebase-functions/v2/https"); +const { msg } = require("@firebase/a-test-pkg"); + +exports.hellov1 = functions.https.onRequest((request, response) => { + response.send(msg); +}); + +exports.hellov2 = onRequest((request, response) => { + response.send(msg); +}); diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/functions/package.json b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/functions/package.json new file mode 100644 index 00000000000..e46713f5d50 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/packages/functions/package.json @@ -0,0 +1,13 @@ +{ + "name": "simple", + "version": "0.0.1", + "dependencies": { + "firebase-functions": "4.0.0", + "firebase-admin": "^11.2.0", + "@firebase/a-test-pkg": "0.0.1" + }, + "engines": { + "node": "20" + }, + "private": true +} diff --git a/scripts/functions-discover-tests/run.sh b/scripts/functions-discover-tests/run.sh new file mode 100755 index 00000000000..92a12377fb3 --- /dev/null +++ b/scripts/functions-discover-tests/run.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +# Globally link the CLI for the testing framework +./scripts/clean-install.sh + +# Unlock internal commands for discovering functions in a project. +firebase experiments:enable internaltesting + +# Install yarn +npm i -g yarn + +# Install pnpm +npm install -g pnpm --force # it's okay to reinstall pnpm + +for dir in ./scripts/functions-discover-tests/fixtures/*; do + (cd $dir && ./install.sh) +done + +mocha scripts/functions-discover-tests/tests.ts \ No newline at end of file diff --git a/scripts/functions-discover-tests/tests.ts b/scripts/functions-discover-tests/tests.ts new file mode 100644 index 00000000000..a50618e8a58 --- /dev/null +++ b/scripts/functions-discover-tests/tests.ts @@ -0,0 +1,117 @@ +import * as path from "path"; + +import { expect } from "chai"; +import { CLIProcess } from "../integration-helpers/cli"; + +const FIXTURES = path.join(__dirname, "fixtures"); +const FIREBASE_PROJECT = "demo-project"; + +interface Testcase { + name: string; + projectDir: string; + expects: { + codebase: string; + endpoints: string[]; + }[]; +} + +describe("Function discovery test", function (this) { + this.timeout(1000_000); + + before(() => { + expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; + }); + + const testCases: Testcase[] = [ + { + name: "simple", + projectDir: "simple", + expects: [ + { + codebase: "default", + endpoints: ["hellov1", "hellov2"], + }, + ], + }, + { + name: "esm", + projectDir: "esm", + expects: [ + { + codebase: "default", + endpoints: ["hellov1", "hellov2"], + }, + ], + }, + { + name: "codebases", + projectDir: "codebases", + expects: [ + { + codebase: "v1", + endpoints: ["hellov1"], + }, + { + codebase: "v2", + endpoints: ["hellov2"], + }, + ], + }, + { + name: "yarn-workspaces", + projectDir: "yarn-workspaces", + expects: [ + { + codebase: "default", + endpoints: ["hellov1", "hellov2"], + }, + ], + }, + { + name: "bundled", + projectDir: "bundled", + expects: [ + { + codebase: "default", + endpoints: ["hello"], + }, + ], + }, + { + name: "pnpm", + projectDir: "pnpm", + expects: [ + { + codebase: "default", + endpoints: ["hellov1", "hellov2"], + }, + ], + }, + ]; + + for (const tc of testCases) { + it(`discovers functions in a ${tc.name} project`, async () => { + const cli = new CLIProcess("default", path.join(FIXTURES, tc.projectDir)); + + let output: any; + await cli.start( + "internaltesting:functions:discover", + FIREBASE_PROJECT, + ["--json"], + (data: any) => { + output = JSON.parse(data); + return true; + }, + ); + expect(output.status).to.equal("success"); + for (const e of tc.expects) { + const endpoints = output.result?.[e.codebase]?.endpoints; + expect(endpoints).to.be.an("object").that.is.not.empty; + expect(Object.keys(endpoints)).to.have.length(e.endpoints.length); + expect(Object.keys(endpoints)).to.include.members(e.endpoints); + } + + await cli.stop(); + }); + } +}); diff --git a/scripts/gen-auth-api-spec.ts b/scripts/gen-auth-api-spec.ts index 32c87f4d62f..94bcf51e823 100644 --- a/scripts/gen-auth-api-spec.ts +++ b/scripts/gen-auth-api-spec.ts @@ -1,6 +1,6 @@ /** * gen-auth-api-specs generates the OpenAPI v3 specification file for the Auth - * Emulator `../src/emulator/auth/apiSpec.js` by converting and combining + * Emulator `../src/emulator/auth/apiSpec.ts` by converting and combining * production Google API Discovery documents for all services it emulates. * * The resulting file can be used with OpenAPI tooling, such as exegesis, a @@ -14,13 +14,10 @@ import * as https from "https"; import { resolve } from "path"; import { writeFileSync } from "fs"; -// @ts-ignore import * as prettier from "prettier"; -// @ts-ignore import * as swagger2openapi from "swagger2openapi"; -// @ts-ignore import { merge, isErrorResult } from "openapi-merge"; -import swaggerToTS from "@manifoldco/swagger-to-ts"; +import openapiTS from "openapi-typescript"; // Convert Google API Discovery format to OpenAPI using this library in order // to use OpenAPI tooling, recommended by https://googleapis.github.io/#openapi. @@ -67,13 +64,14 @@ async function main(): Promise { "/* See README.md (Section: Autogenerated files) for how to read / review this file. */\n" + "/* eslint-disable */\n\n"; const specContent = header + "export default " + JSON.stringify(merged.output); - const specFile = resolve(__dirname, "../src/emulator/auth/apiSpec.js"); + const specFile = resolve(__dirname, "../src/emulator/auth/apiSpec.ts"); const prettierOptions = await prettier.resolveConfig(specFile); writeFileSync(specFile, prettier.format(specContent, { ...prettierOptions, filepath: specFile })); // Also generate TypeScript definitions for use in implementation. - const prettierConfig = resolve(__dirname, "../.prettierrc"); - const defsContent = header + swaggerToTS(merged.output as any, { prettierConfig }); + const prettierConfig = resolve(__dirname, "../.prettierrc.js"); + const output = await openapiTS(merged.output as any, { prettierConfig }); + const defsContent = header + output; writeFileSync(resolve(__dirname, "../src/emulator/auth/schema.ts"), defsContent); } @@ -116,10 +114,10 @@ async function toOpenapi3(discovery: Discovery): Promise { // tools offer one single API call for the entire conversion, but perform // indirect conversion under the hood. We'll just do it explicitly and that // also gives us more control (such as .setStrict above) and less deps. - const swagger = await googleDiscoveryToSwagger.convert(discovery); + const swagger: any = await googleDiscoveryToSwagger.convert(discovery); const result = await swagger2openapi.convertObj(swagger, {}); const openapi3 = result.openapi; - openapi3.servers.forEach((server: { url: string }) => { + openapi3.servers?.forEach((server: { url: string }) => { // Server URL should not end with slash since it is prefixed to paths. server.url = server.url.replace(/\/$/, ""); }); @@ -187,6 +185,7 @@ const paramPattern = /{([^}]+)}/g; function replaceWithFlatPath(discovery: Resource | Resources): void { if (discovery.methods) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars Object.entries(discovery.methods).forEach(([_, method]) => { // Replace flat path param names with path param names // e.g. for endpoint identitytoolkit.projects.defaultSupportedIdpConfigs.get: @@ -251,13 +250,20 @@ function patchSecurity(openapi3: any, apiKeyDescription: string): void { securitySchemes = openapi3.components.securitySchemes = {}; } - // Add the missing apiKey method here. - securitySchemes.apiKey = { + // Add the missing apiKeyQuery and apiKeyHeader schemes here. + // https://cloud.google.com/docs/authentication/api-keys#using-with-rest + securitySchemes.apiKeyQuery = { type: "apiKey", name: "key", in: "query", description: apiKeyDescription, }; + securitySchemes.apiKeyHeader = { + type: "apiKey", + name: "x-goog-api-key", + in: "header", + description: apiKeyDescription, + }; forEachOperation(openapi3, (operation) => { if (!operation.security) { @@ -270,9 +276,9 @@ function patchSecurity(openapi3: any, apiKeyDescription: string): void { delete alt.Oauth2c; }); - // Forcibly add API Key as an alternative auth method. Note that some - // operations may not support it, but those can be handled within impl. - operation.security.push({ apiKey: [] }); + // Add alternative auth schemes (query OR header) for API key. Note that + // some operations may not support it, but those can be handled within impl. + operation.security.push({ apiKeyQuery: [] }, { apiKeyHeader: [] }); }); } @@ -448,10 +454,6 @@ function addEmulatorOperations(openapi3: any): void { }, type: "object", }, - usageMode: { - enum: ["USAGE_MODE_UNSPECIFIED", "DEFAULT", "PASSTHROUGH"], - type: "string", - }, }, }; openapi3.paths["/emulator/v1/projects/{targetProjectId}/oobCodes"] = { @@ -643,7 +645,7 @@ function sortKeys(obj: T): T { return obj; } if (Array.isArray(obj)) { - return (obj.map(sortKeys) as unknown) as T; + return obj.map(sortKeys) as unknown as T; } const sortedObj: T = {} as T; (Object.keys(obj) as [keyof T]).sort().forEach((key) => { diff --git a/scripts/hosting-tests/rewrites-tests/run.sh b/scripts/hosting-tests/rewrites-tests/run.sh new file mode 100755 index 00000000000..ab51f1d0231 --- /dev/null +++ b/scripts/hosting-tests/rewrites-tests/run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +source scripts/set-default-credentials.sh + +mocha scripts/hosting-tests/rewrites-tests/tests.ts diff --git a/scripts/hosting-tests/rewrites-tests/tests.ts b/scripts/hosting-tests/rewrites-tests/tests.ts new file mode 100644 index 00000000000..e393bd0a4a6 --- /dev/null +++ b/scripts/hosting-tests/rewrites-tests/tests.ts @@ -0,0 +1,1007 @@ +import { expect } from "chai"; +import { join } from "path"; +import { writeFileSync, emptyDirSync, ensureDirSync } from "fs-extra"; +import * as tmp from "tmp"; + +import * as firebase from "../../../src"; +import { execSync } from "child_process"; +import { command as functionsDelete } from "../../../src/commands/functions-delete"; +import fetch, { Request } from "node-fetch"; +import { FirebaseError } from "../../../src/error"; + +tmp.setGracefulCleanup(); + +// Run this test manually by: +// - Setting the target project to any project that can create publicly invokable functions. +// - Disabling mockAuth in .mocharc + +const functionName = `helloWorld_${process.env.CI_RUN_ID || "XX"}_${ + process.env.CI_RUN_ATTEMPT || "YY" +}`; + +// Typescript doesn't like calling functions on `firebase`. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const client: any = firebase; + +function writeFirebaseRc(firebasercFilePath: string): void { + const config = { + projects: { + default: process.env.FBTOOLS_TARGET_PROJECT, + }, + targets: { + [process.env.FBTOOLS_TARGET_PROJECT as string]: { + hosting: { + "client-integration-site": [process.env.FBTOOLS_CLIENT_INTEGRATION_SITE], + }, + }, + }, + }; + writeFileSync(firebasercFilePath, JSON.stringify(config)); +} + +async function deleteDeployedFunctions(): Promise { + try { + await functionsDelete.runner()([functionName], { + projectId: process.env.FBTOOLS_TARGET_PROJECT, + force: true, + }); + } catch (FirebaseError) { + // do nothing if the function doesn't match. + } +} + +function functionRegionString(functionRegions: string[]): string { + const functionRegionsQuoted = functionRegions.map((regionString) => { + return `"${regionString}"`; + }); + return functionRegionsQuoted.join(","); +} + +function writeHelloWorldFunctionWithRegions( + functionName: string, + functionsDirectory: string, + functionRegions?: string[], +): void { + ensureDirSync(functionsDirectory); + + const region = functionRegions ? `.region(${functionRegionString(functionRegions)})` : ""; + const functionFileContents = ` +const functions = require("firebase-functions"); + +exports.${functionName} = functions${region}.https.onRequest((request, response) => { + functions.logger.info("Hello logs!", { structuredData: true }); + const envVarFunctionsRegion = process.env.FUNCTION_REGION; + response.send("Hello from Firebase ${ + functionRegions ? "from " + functionRegions.toString() : "" + }"); +});`; + writeFileSync(join(functionsDirectory, ".", "index.js"), functionFileContents); + + const functionsPackage = { + name: "functions", + engines: { + node: "16", + }, + main: "index.js", + dependencies: { + "firebase-admin": "^10.0.2", + "firebase-functions": "^3.18.0", + }, + private: true, + }; + writeFileSync(join(functionsDirectory, ".", "package.json"), JSON.stringify(functionsPackage)); + execSync("npm install", { cwd: functionsDirectory }); +} + +function writeBasicHostingFile(hostingDirectory: string): void { + writeFileSync( + join(hostingDirectory, ".", "index.html"), + `< !DOCTYPE html > + + + +< body > +Rabbit +< /body> +< /html>`, + ); +} + +class TempDirectoryInfo { + tempDir = tmp.dirSync({ prefix: "hosting_rewrites_tests_" }); + firebasercFilePath = join(this.tempDir.name, ".", ".firebaserc"); + hostingDirPath = join(this.tempDir.name, ".", "hosting"); +} + +describe("deploy function-targeted rewrites And functions", () => { + let tempDirInfo = new TempDirectoryInfo(); + + // eslint-disable-next-line prefer-arrow-callback + beforeEach(async function () { + tempDirInfo = new TempDirectoryInfo(); + // eslint-disable-next-line @typescript-eslint/no-invalid-this + this.timeout(100 * 1e3); + await deleteDeployedFunctions(); + emptyDirSync(tempDirInfo.tempDir.name); + writeFirebaseRc(tempDirInfo.firebasercFilePath); + }); + + afterEach(async function () { + // eslint-disable-next-line @typescript-eslint/no-invalid-this + this.timeout(100 * 1e3); + await deleteDeployedFunctions(); + }); + + after(async function () { + // eslint-disable-next-line @typescript-eslint/no-invalid-this + this.timeout(100 * 1e3); + await deleteDeployedFunctions(); + }); + + it("should deploy with default function region", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + }).timeout(1000 * 1e3); + + it("should deploy with default function region explicitly specified in rewrite", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "us-central1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + }).timeout(1000 * 1e3); + + it("should deploy with autodetected (not us-central1) function region", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + }).timeout(1000 * 1e3); + + it("should deploy rewrites and functions with function region specified in both", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + }).timeout(1000 * 1e3); + + it("should fail to deploy rewrites with the wrong function region", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west2"], + ); + + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }), + ).to.eventually.be.rejectedWith(FirebaseError, "Unable to find a valid endpoint for function"); + }).timeout(1000 * 1e3); + + it("should fail to deploy rewrites to a function being deleted in a region", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions", + force: true, + }); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west1"], + ); + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions,hosting", + force: true, + }), + ).to.eventually.be.rejectedWith(FirebaseError, "Unable to find a valid endpoint for function"); + }).timeout(1000 * 1e3); + + it("should deploy when a rewrite points to a non-existent function", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: "function-that-doesnt-exist", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + }).timeout(1000 * 1e3); + + it("should rewrite using a specified function region for a function with multiple regions", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1", "europe-west1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + }).timeout(1000 * 1e3); + + it("should rewrite to the default of us-central1 if multiple regions including us-central1 are available", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1", "us-central1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + }).timeout(1000 * 1e3); + + it("should fail when rewrite points to an invalid region for a function with multiple regions", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "us-east1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1", "europe-west1"], + ); + + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + }), + ).to.eventually.be.rejectedWith(FirebaseError, "Unable to find a valid endpoint for function"); + }).timeout(1000 * 1e3); + + it("should fail when rewrite has no region specified for a function with multiple regions", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1", "europe-west1"], + ); + + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }), + ).to.eventually.be.rejectedWith(FirebaseError, "More than one backend found for function"); + }).timeout(1000 * 1e3); + + it("should deploy with autodetected function region when function region is changed", async () => { + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + const responseText = await functionsResponse.text(); + expect(responseText).to.contain("Hello from Firebase"); + expect(responseText).to.contain("europe-west1"); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse2 = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse2.text()).to.contain("Rabbit"); + const functionsResponse2 = await fetch(functionsRequest); + const responseText2 = await functionsResponse2.text(); + + expect(responseText2).to.contain("Hello from Firebase"); + expect(responseText2).to.contain("asia-northeast1"); + expect(responseText2).not.to.contain("europe-west1"); + }).timeout(1000 * 1e3); + + it("should deploy with specified function region when function region is changed", async () => { + let firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "europe-west1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west1"], + ); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + { + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsResponse = await fetch(functionsRequest); + + const responseText = await functionsResponse.text(); + expect(responseText).to.contain("Hello from Firebase"); + expect(responseText).to.contain("europe-west1"); + } + + // Change function region in both firebase.json and function definition. + firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1"], + ); + + { + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + const functionsResponse = await fetch(functionsRequest); + const responseText = await functionsResponse.text(); + + expect(responseText).to.contain("Hello from Firebase"); + expect(responseText).to.contain("asia-northeast1"); + expect(responseText).not.to.contain("europe-west1"); + } + }).timeout(1000 * 1e3); + + it("should fail to deploy when rewrite function region changes and actual function region doesn't", async () => { + let firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "europe-west1", + }, + ], + }, + }; + + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west1"], + ); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + { + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting,functions", + force: true, + }); + + const staticResponse = await fetch( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/index.html`, + ); + expect(await staticResponse.text()).to.contain("Rabbit"); + + const functionsResponse = await fetch(functionsRequest); + + const responseText = await functionsResponse.text(); + expect(responseText).to.contain("Hello from Firebase"); + expect(responseText).to.contain("europe-west1"); + } + + // Change function region in both firebase.json. + firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + { + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting", + force: true, + }), + ).to.eventually.be.rejectedWith(FirebaseError); + } + }).timeout(1000 * 1e3); + + it("should fail to deploy when target function doesn't exist in specified region and isn't being deployed to that region", async () => { + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, "{}"); + + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["europe-west1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions", + force: true, + }); + + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + writeBasicHostingFile(tempDirInfo.hostingDirPath); + + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting", + force: true, + }), + ).to.eventually.be.rejectedWith(FirebaseError); + }).timeout(1000 * 1e3); + + it("should deploy when target function exists in prod but code isn't available", async () => { + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, "{}"); + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions", + force: true, + }); + + emptyDirSync(join(tempDirInfo.tempDir.name, ".", "functions")); + ensureDirSync(tempDirInfo.hostingDirPath); + + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + }, + ], + }, + }; + + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + emptyDirSync(join(tempDirInfo.tempDir.name, ".", "functions")); + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting", // Including functions here will prompt for deletion. + // Forcing the prompt will delete the function. + }); + + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + }).timeout(1000 * 1e3); + + it("should fail to deploy when target function exists in prod, code isn't available, and rewrite region is specified incorrectly", async () => { + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1"], + ); + writeFileSync(firebaseJsonFilePath, "{}"); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions", + force: true, + }); + + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "europe-west1", + }, + ], + }, + }; + + emptyDirSync(join(tempDirInfo.tempDir.name, ".", "functions")); + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + + await expect( + client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting", // Including functions here will prompt for deletion. + // Forcing the prompt will delete the function. + }), + ).to.eventually.be.rejectedWith(FirebaseError); + }).timeout(1000 * 1e3); + + it("should deploy when target function exists in prod, codebase isn't available, and region matches", async () => { + const firebaseJsonFilePath = join(tempDirInfo.tempDir.name, ".", "firebase.json"); + writeFileSync(firebaseJsonFilePath, "{}"); + writeHelloWorldFunctionWithRegions( + functionName, + join(tempDirInfo.tempDir.name, ".", "functions"), + ["asia-northeast1"], + ); + + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "functions", + force: true, + }); + + emptyDirSync(join(tempDirInfo.tempDir.name, ".", "functions")); + const firebaseJson = { + hosting: { + public: "hosting", + rewrites: [ + { + source: "/helloWorld", + function: functionName, + region: "asia-northeast1", + }, + ], + }, + }; + writeFileSync(firebaseJsonFilePath, JSON.stringify(firebaseJson)); + ensureDirSync(tempDirInfo.hostingDirPath); + await client.deploy({ + project: process.env.FBTOOLS_TARGET_PROJECT, + cwd: tempDirInfo.tempDir.name, + only: "hosting", // Including functions here will prompt for deletion. + // Forcing the prompt will delete the function. + }); + + { + const functionsRequest = new Request( + `https://${process.env.FBTOOLS_CLIENT_INTEGRATION_SITE}.web.app/helloWorld`, + ); + + const functionsResponse = await fetch(functionsRequest); + expect(await functionsResponse.text()).to.contain("Hello from Firebase"); + } + }).timeout(1000 * 1e3); +}).timeout(1000 * 1e3); diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index 545a918902a..6ca9616917f 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -22,13 +22,19 @@ TEMP_DIR="$(mktemp -d)" echo "Created temp directory: ${TEMP_DIR}" echo "Installing firebase-tools..." -./scripts/npm-link.sh +./scripts/clean-install.sh echo "Installed firebase-tools: $(which firebase)" echo "Initializing temp directory..." cd "${TEMP_DIR}" +PORT=8685 cat > "firebase.json" <<- EOM { + "emulators": { + "hosting": { + "port": "${PORT}" + } + }, "hosting": { "public": "public", "ignore": [ @@ -45,7 +51,6 @@ echo "${DATE}" > "public/${TARGET_FILE}" echo "Initialized temp directory." echo "Testing local serve..." -PORT=8685 firebase serve --only hosting --project "${FBTOOLS_TARGET_PROJECT}" --port "${PORT}" & PID="$!" sleep 5 @@ -56,7 +61,6 @@ wait echo "Tested local serve." echo "Testing local hosting emulator..." -PORT=5000 firebase emulators:start --only hosting --project "${FBTOOLS_TARGET_PROJECT}" & PID="$!" sleep 5 @@ -76,14 +80,15 @@ wait echo "Tested local hosting emulator." echo "Testing hosting deployment..." -firebase deploy --only hosting --project "${FBTOOLS_TARGET_PROJECT}" +firebase hosting:channel:deploy --expires 1h --project "${FBTOOLS_TARGET_PROJECT}" --json "${GITHUB_RUN_NUMBER}" | tee channeldeploy.json +URL=$(cat channeldeploy.json | jq -r ".result.\"${FBTOOLS_TARGET_PROJECT}\".url") sleep 12 -VALUE="$(curl https://${FBTOOLS_TARGET_PROJECT}.web.app/${TARGET_FILE})" +VALUE="$(curl $URL/${TARGET_FILE})" test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false) # Test that ?useEmulator has no effect on init.js -INIT_JS_NONE="$(curl https://${FBTOOLS_TARGET_PROJECT}.web.app/__/firebase/init.js)" -INIT_JS_TRUE="$(curl https://${FBTOOLS_TARGET_PROJECT}.web.app/__/firebase/init.js\?useEmulator=true)" +INIT_JS_NONE="$(curl $URL/__/firebase/init.js)" +INIT_JS_TRUE="$(curl $URL/__/firebase/init.js\?useEmulator=true)" test "${INIT_JS_NONE}" = "${INIT_JS_TRUE}" || (echo "Expected ${INIT_JS_NONE} to equal ${INIT_JS_TRUE}." && false) echo "Tested hosting deployment." @@ -123,17 +128,18 @@ firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." -echo "Testing hosting deployment by target..." -firebase deploy --only hosting:customtarget --project "${FBTOOLS_TARGET_PROJECT}" -sleep 12 -VALUE="$(curl https://${FBTOOLS_TARGET_PROJECT}.web.app/${TARGET_FILE})" -test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false) -echo "Tested hosting deployment by target." +# Skipping this in favor of the test below. +# echo "Testing hosting deployment by target..." +# firebase deploy --only hosting:customtarget --project "${FBTOOLS_TARGET_PROJECT}" +# VALUE="$(curl https://${FBTOOLS_TARGET_PROJECT}.web.app/${TARGET_FILE})" +# sleep 12 +# test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false) +# echo "Tested hosting deployment by target." echo "Testing hosting channel deployment by target..." firebase hosting:channel:deploy mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --json | tee output.json -sleep 12 CHANNEL_URL=$(cat output.json | jq -r ".result.customtarget.url") +sleep 12 VALUE="$(curl ${CHANNEL_URL}/${TARGET_FILE})" test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false) echo "Tested hosting channel deployment by target." diff --git a/scripts/integration-helpers/cli.ts b/scripts/integration-helpers/cli.ts index 53192ef8aba..8f603946e03 100644 --- a/scripts/integration-helpers/cli.ts +++ b/scripts/integration-helpers/cli.ts @@ -1,15 +1,19 @@ -import * as subprocess from "child_process"; +import { ChildProcess } from "child_process"; +import * as spawn from "cross-spawn"; export class CLIProcess { - process?: subprocess.ChildProcess; + process?: ChildProcess; - constructor(private readonly name: string, private readonly workdir: string) {} + constructor( + private readonly name: string, + private readonly workdir: string, + ) {} start( cmd: string, project: string, additionalArgs: string[], - logDoneFn?: (d: unknown) => unknown + logDoneFn?: (d: unknown) => unknown, ): Promise { const args = [cmd, "--project", project]; @@ -17,17 +21,17 @@ export class CLIProcess { args.push(...additionalArgs); } - const p = subprocess.spawn("firebase", args, { cwd: this.workdir }); + const p = spawn("firebase", args, { cwd: this.workdir }); if (!p) { throw new Error("Failed to start firebase CLI"); } this.process = p; - this.process.stdout.on("data", (data: unknown) => { + this.process.stdout?.on("data", (data: unknown) => { process.stdout.write(`[${this.name} stdout] ` + data); }); - this.process.stderr.on("data", (data: unknown) => { + this.process.stderr?.on("data", (data: unknown) => { console.log(`[${this.name} stderr] ` + data); }); @@ -37,16 +41,16 @@ export class CLIProcess { const customCallback = (data: unknown): void => { if (logDoneFn(data)) { // eslint-disable-next-line @typescript-eslint/no-use-before-define - p.stdout.removeListener("close", customFailure); + p.stdout?.removeListener("close", customFailure); resolve(); } }; const customFailure = (): void => { - p.stdout.removeListener("data", customCallback); + p.stdout?.removeListener("data", customCallback); reject(new Error("failed to resolve startup before process.stdout closed")); }; - p.stdout.on("data", customCallback); - p.stdout.on("close", customFailure); + p.stdout?.on("data", customCallback); + p.stdout?.on("close", customFailure); }); } else { started = new Promise((resolve) => { @@ -66,7 +70,7 @@ export class CLIProcess { return Promise.resolve(); } - const stopped = new Promise((resolve) => { + const stopped = new Promise((resolve) => { p.once("exit", (/* exitCode, signal */) => { this.process = undefined; resolve(); diff --git a/scripts/integration-helpers/framework.ts b/scripts/integration-helpers/framework.ts index 6afdda54c12..038a7615194 100644 --- a/scripts/integration-helpers/framework.ts +++ b/scripts/integration-helpers/framework.ts @@ -1,6 +1,7 @@ import fetch, { Response } from "node-fetch"; import { CLIProcess } from "./cli"; +import { Emulators } from "../../src/emulator/types"; const FIREBASE_PROJECT_ZONE = "us-central1"; @@ -22,6 +23,8 @@ const STORAGE_BUCKET_FUNCTION_V2_FINALIZED_LOG = "========== STORAGE BUCKET V2 FUNCTION FINALIZED =========="; const STORAGE_BUCKET_FUNCTION_V2_METADATA_LOG = "========== STORAGE BUCKET V2 FUNCTION METADATA =========="; +const RTDB_V2_FUNCTION_LOG = "========== RTDB V2 FUNCTION =========="; +const FIRESTORE_V2_LOG = "========== FIRESTORE V2 FUNCTION =========="; /* Functions V1 */ const RTDB_FUNCTION_LOG = "========== RTDB FUNCTION =========="; const FIRESTORE_FUNCTION_LOG = "========== FIRESTORE FUNCTION =========="; @@ -39,6 +42,10 @@ const STORAGE_BUCKET_FUNCTION_FINALIZED_LOG = const STORAGE_BUCKET_FUNCTION_METADATA_LOG = "========== STORAGE BUCKET FUNCTION METADATA =========="; const ALL_EMULATORS_STARTED_LOG = "All emulators ready"; +const AUTH_BLOCKING_CREATE_V2_LOG = + "========== AUTH BLOCKING CREATE V2 FUNCTION METADATA =========="; +const AUTH_BLOCKING_SIGN_IN_V2_LOG = + "========== AUTH BLOCKING SIGN IN V2 FUNCTION METADATA =========="; interface ConnectionInfo { host: string; @@ -47,6 +54,7 @@ interface ConnectionInfo { export interface FrameworkOptions { emulators?: { + hub: ConnectionInfo; database: ConnectionInfo; firestore: ConnectionInfo; functions: ConnectionInfo; @@ -56,21 +64,60 @@ export interface FrameworkOptions { }; } -export class TriggerEndToEndTest { - rtdbEmulatorHost = "localhost"; +export class EmulatorEndToEndTest { + emulatorHubPort = 0; + rtdbEmulatorHost = "127.0.0.1"; rtdbEmulatorPort = 0; - firestoreEmulatorHost = "localhost"; + firestoreEmulatorHost = "127.0.0.1"; firestoreEmulatorPort = 0; - functionsEmulatorHost = "localhost"; + functionsEmulatorHost = "127.0.0.1"; functionsEmulatorPort = 0; - pubsubEmulatorHost = "localhost"; + pubsubEmulatorHost = "127.0.0.1"; pubsubEmulatorPort = 0; - authEmulatorHost = "localhost"; + authEmulatorHost = "127.0.0.1"; authEmulatorPort = 0; - storageEmulatorHost = "localhost"; + storageEmulatorHost = "127.0.0.1"; storageEmulatorPort = 0; allEmulatorsStarted = false; + cliProcess?: CLIProcess; + + constructor( + public project: string, + protected readonly workdir: string, + config: FrameworkOptions, + ) { + if (!config.emulators) { + return; + } + this.emulatorHubPort = config.emulators.hub?.port; + this.rtdbEmulatorPort = config.emulators.database?.port; + this.firestoreEmulatorPort = config.emulators.firestore?.port; + this.functionsEmulatorPort = config.emulators.functions?.port; + this.pubsubEmulatorPort = config.emulators.pubsub?.port; + this.authEmulatorPort = config.emulators.auth?.port; + this.storageEmulatorPort = config.emulators.storage?.port; + } + + startEmulators(additionalArgs: string[] = []): Promise { + const cli = new CLIProcess("default", this.workdir); + const started = cli.start("emulators:start", this.project, additionalArgs, (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(ALL_EMULATORS_STARTED_LOG); + }); + + this.cliProcess = cli; + return started; + } + + stopEmulators(): Promise { + return this.cliProcess ? this.cliProcess.stop() : Promise.resolve(); + } +} + +export class TriggerEndToEndTest extends EmulatorEndToEndTest { /* Functions V1 */ rtdbTriggerCount = 0; firestoreTriggerCount = 0; @@ -84,6 +131,8 @@ export class TriggerEndToEndTest { storageBucketDeletedTriggerCount = 0; storageBucketFinalizedTriggerCount = 0; storageBucketMetadataTriggerCount = 0; + authBlockingCreateV1TriggerCount = 0; + authBlockingSignInV1TriggerCount = 0; /* Functions V2 */ pubsubV2TriggerCount = 0; @@ -95,23 +144,15 @@ export class TriggerEndToEndTest { storageBucketV2DeletedTriggerCount = 0; storageBucketV2FinalizedTriggerCount = 0; storageBucketV2MetadataTriggerCount = 0; + authBlockingCreateV2TriggerCount = 0; + authBlockingSignInV2TriggerCount = 0; + rtdbV2TriggerCount = 0; + firestoreV2TriggerCount = 0; rtdbFromFirestore = false; firestoreFromRtdb = false; rtdbFromRtdb = false; firestoreFromFirestore = false; - cliProcess?: CLIProcess; - - constructor(public project: string, private readonly workdir: string, config: FrameworkOptions) { - if (config.emulators) { - this.rtdbEmulatorPort = config.emulators.database?.port; - this.firestoreEmulatorPort = config.emulators.firestore?.port; - this.functionsEmulatorPort = config.emulators.functions?.port; - this.pubsubEmulatorPort = config.emulators.pubsub?.port; - this.authEmulatorPort = config.emulators.auth?.port; - this.storageEmulatorPort = config.emulators.storage?.port; - } - } resetCounts(): void { /* Functions V1 */ @@ -127,6 +168,8 @@ export class TriggerEndToEndTest { this.storageBucketDeletedTriggerCount = 0; this.storageBucketFinalizedTriggerCount = 0; this.storageBucketMetadataTriggerCount = 0; + this.authBlockingCreateV1TriggerCount = 0; + this.authBlockingSignInV1TriggerCount = 0; /* Functions V2 */ this.pubsubV2TriggerCount = 0; @@ -138,6 +181,10 @@ export class TriggerEndToEndTest { this.storageBucketV2DeletedTriggerCount = 0; this.storageBucketV2FinalizedTriggerCount = 0; this.storageBucketV2MetadataTriggerCount = 0; + this.authBlockingCreateV2TriggerCount = 0; + this.authBlockingSignInV2TriggerCount = 0; + this.rtdbV2TriggerCount = 0; + this.firestoreV2TriggerCount = 0; } /* @@ -153,16 +200,11 @@ export class TriggerEndToEndTest { ); } - startEmulators(additionalArgs: string[] = []): Promise { - const cli = new CLIProcess("default", this.workdir); - const started = cli.start("emulators:start", this.project, additionalArgs, (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - }); + async startEmulators(additionalArgs: string[] = []): Promise { + // This must be called first to set this.cliProcess. + const startEmulators = super.startEmulators(additionalArgs); - cli.process?.stdout.on("data", (data) => { + this.cliProcess?.process?.stdout?.on("data", (data) => { /* Functions V1 */ if (data.includes(RTDB_FUNCTION_LOG)) { this.rtdbTriggerCount++; @@ -200,6 +242,7 @@ export class TriggerEndToEndTest { if (data.includes(STORAGE_BUCKET_FUNCTION_METADATA_LOG)) { this.storageBucketMetadataTriggerCount++; } + /* Functions V2 */ if (data.includes(PUBSUB_FUNCTION_V2_LOG)) { this.pubsubV2TriggerCount++; @@ -228,10 +271,21 @@ export class TriggerEndToEndTest { if (data.includes(STORAGE_BUCKET_FUNCTION_V2_METADATA_LOG)) { this.storageBucketV2MetadataTriggerCount++; } + if (data.includes(AUTH_BLOCKING_CREATE_V2_LOG)) { + this.authBlockingCreateV2TriggerCount++; + } + if (data.includes(AUTH_BLOCKING_SIGN_IN_V2_LOG)) { + this.authBlockingSignInV2TriggerCount++; + } + if (data.includes(RTDB_V2_FUNCTION_LOG)) { + this.rtdbV2TriggerCount++; + } + if (data.includes(FIRESTORE_V2_LOG)) { + this.firestoreV2TriggerCount++; + } }); - this.cliProcess = cli; - return started; + return startEmulators; } startExtEmulators(additionalArgs: string[]): Promise { @@ -241,28 +295,64 @@ export class TriggerEndToEndTest { this.project, additionalArgs, (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { throw new Error(`data is not a string or buffer (${typeof data})`); } return data.includes(ALL_EMULATORS_STARTED_LOG); - } + }, ); this.cliProcess = cli; return started; } - stopEmulators(): Promise { - return this.cliProcess ? this.cliProcess.stop() : Promise.resolve(); + applyTargets(emulatorType: Emulators, target: string, resource: string): Promise { + const cli = new CLIProcess("default", this.workdir); + const started = cli.start( + "target:apply", + this.project, + [emulatorType, target, resource], + (data: unknown) => { + if (typeof data !== "string" && !Buffer.isBuffer(data)) { + throw new Error(`data is not a string or buffer (${typeof data})`); + } + return data.includes(`Applied ${emulatorType} target`); + }, + ); + this.cliProcess = cli; + return started; } invokeHttpFunction(name: string, zone = FIREBASE_PROJECT_ZONE): Promise { - const url = `http://localhost:${[this.functionsEmulatorPort, this.project, zone, name].join( - "/" + const url = `http://127.0.0.1:${[this.functionsEmulatorPort, this.project, zone, name].join( + "/", )}`; return fetch(url); } + invokeCallableFunction( + name: string, + body: Record, + zone = FIREBASE_PROJECT_ZONE, + ): Promise { + const url = `http://127.0.0.1:${this.functionsEmulatorPort}/${[this.project, zone, name].join( + "/", + )}`; + return fetch(url, { + method: "POST", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + }); + } + + createUserFromAuth(): Promise { + return this.invokeHttpFunction("createUserFromAuth"); + } + + signInUserFromAuth(): Promise { + return this.invokeHttpFunction("signInUserFromAuth"); + } + writeToRtdb(): Promise { return this.invokeHttpFunction("writeToRtdb"); } @@ -291,6 +381,14 @@ export class TriggerEndToEndTest { return this.invokeHttpFunction("writeToSpecificStorageBucket"); } + updateMetadataDefaultStorage(): Promise { + return this.invokeHttpFunction("updateMetadataFromDefaultStorage"); + } + + updateMetadataSpecificStorageBucket(): Promise { + return this.invokeHttpFunction("updateMetadataFromSpecificStorageBucket"); + } + updateDeleteFromDefaultStorage(): Promise { return this.invokeHttpFunction("updateDeleteFromDefaultStorage"); } @@ -302,7 +400,7 @@ export class TriggerEndToEndTest { waitForCondition( conditionFn: () => boolean, timeout: number, - callback: (err?: Error) => void + callback: (err?: Error) => void, ): void { let elapsed = 0; const interval = 10; @@ -320,4 +418,14 @@ export class TriggerEndToEndTest { } }, interval); } + + disableBackgroundTriggers(): Promise { + const url = `http://127.0.0.1:${this.emulatorHubPort}/functions/disableBackgroundTriggers`; + return fetch(url, { method: "PUT" }); + } + + enableBackgroundTriggers(): Promise { + const url = `http://127.0.0.1:${this.emulatorHubPort}/functions/enableBackgroundTriggers`; + return fetch(url, { method: "PUT" }); + } } diff --git a/scripts/lint-changed-files.ts b/scripts/lint-changed-files.ts index 8b48792aec2..84d9f837795 100644 --- a/scripts/lint-changed-files.ts +++ b/scripts/lint-changed-files.ts @@ -70,7 +70,7 @@ function main(): void { cwd: root, stdio: ["pipe", process.stdout, process.stderr], }); - } catch (e) { + } catch (e: any) { console.error("eslint failed, see errors above."); console.error(); process.exit(e.status); diff --git a/scripts/npm-link.sh b/scripts/npm-link.sh deleted file mode 100755 index 26e3276d211..00000000000 --- a/scripts/npm-link.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -e - -if [ "$CI" = "true" ]; then - echo "Running sudo npm link..." - sudo npm link -else - echo "Running npm link..." - npm link -fi diff --git a/scripts/publish-vsce.sh b/scripts/publish-vsce.sh new file mode 100755 index 00000000000..e9eee09ae83 --- /dev/null +++ b/scripts/publish-vsce.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e + +printusage() { + echo "publish-vsce.sh " + echo "Should be run as part of publish.sh." + echo "" + echo "" + echo "Arguments:" + echo " version: 'patch', 'minor', or 'major'." + echo " cli-version-number: the version number of the CLI code that is bundled in this release." +} + +VERSION=$1 + +CLI_VERSION=$2 +if [[ ($VERSION == "" || $CLI_VERSION == "") ]]; then + printusage + exit 1 +elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]]; then + printusage + exit 1 +fi + +cd firebase-vscode + +echo "Making a $VERSION version of VSCode..." +npm version $VERSION +NEW_VSCODE_VERSION=$(jq -r ".version" package.json) +NEXT_HEADER="## NEXT" +NEW_HEADER="## NEXT\n\n## $NEW_VSCODE_VERSION\n\n- Updated internal firebase-tools dependency to $CLI_VERSION" +sed -i -e "s/$NEXT_HEADER/$NEW_HEADER/g" CHANGELOG.md +echo "Made a $VERSION version of VSCode." + +echo "Running npm install for VSCode..." +npm install +echo "Ran npm install for VSCode." + +echo "Building firebase-vscode .VSIX file" +npm run pkg +echo "Built firebase-vscode .VSIX file." + +echo "Uploading VSIX file to GCS..." +VSIX="firebase-vscode-$NEW_VSCODE_VERSION.vsix" +gsutil cp $VSIX gs://firemat-preview-drop/vsix/$VSIX +gsutil cp $VSIX gs://firemat-preview-drop/vsix/firebase-vscode-latest.vsix +echo "Uploaded VSIX file to GCS." +cd .. \ No newline at end of file diff --git a/scripts/publish.sh b/scripts/publish.sh old mode 100644 new mode 100755 index 70c6e361a30..ab990d66a50 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -2,12 +2,13 @@ set -e printusage() { - echo "publish.sh " + echo "publish.sh [vscode-version]" echo "REPOSITORY_ORG and REPOSITORY_NAME should be set in the environment." echo "e.g. REPOSITORY_ORG=user, REPOSITORY_NAME=repo" echo "" echo "Arguments:" echo " version: 'patch', 'minor', or 'major'." + echo " vscode-version: Optional. If omitted, defaults to . May be 'patch', 'minor', or 'major'." } VERSION=$1 @@ -19,6 +20,14 @@ elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]] exit 1 fi +VSCODE_VERSION=$2 +if [[ $VSCODE_VERSION == "" ]]; then + VSCODE_VERSION=$VERSION +elif [[ ! ($VSCODE_VERSION == "patch" || $VSCODE_VERSION == "minor" || $VSCODE_VERSION == "major") ]]; then + printusage + exit 1 +fi + if [[ $REPOSITORY_ORG == "" ]]; then printusage exit 1 @@ -42,13 +51,11 @@ trap - ERR trap "echo 'Missing jq.'; exit 1" ERR which jq &> /dev/null trap - ERR -echo "Checked for commands." -echo "Checking for Twitter credentials..." -trap "echo 'Missing Twitter credentials.'; exit 1" ERR -test -f "${WDIR}/scripts/twitter.json" +trap "echo 'Missing gsutil.'; exit 1" ERR +which gsutil &> /dev/null trap - ERR -echo "Checked for Twitter credentials..." +echo "Checked for commands." echo "Checking for logged-in npm user..." trap "echo 'Please login to npm using \`npm login --registry https://wombat-dressing-room.appspot.com\`'; exit 1" ERR @@ -87,6 +94,10 @@ npm version $VERSION NEW_VERSION=$(jq -r ".version" package.json) echo "Made a $VERSION version." +echo "Publishing a $VSCODE_VERSION version of the VSCode extension..." +bash ./scripts/publish-vsce.sh $VSCODE_VERSION $NEW_VERSION +echo "Published a $VSCODE_VERSION version of the VSCode extension." + echo "Making the release notes..." RELEASE_NOTES_FILE=$(mktemp) echo "[DEBUG] ${RELEASE_NOTES_FILE}" @@ -96,13 +107,13 @@ cat CHANGELOG.md >> "${RELEASE_NOTES_FILE}" echo "Made the release notes." echo "Publishing to npm..." -npm publish +npx clean-publish --before-script ./scripts/clean-shrinkwrap.sh echo "Published to npm." echo "Cleaning up release notes..." rm CHANGELOG.md touch CHANGELOG.md -git commit -m "[firebase-release] Removed change log and reset repo after ${NEW_VERSION} release" CHANGELOG.md +git commit -m "[firebase-release] Removed change log and reset repo after ${NEW_VERSION} release" CHANGELOG.md firebase-vscode/CHANGELOG.md firebase-vscode/package.json firebase-vscode/package-lock.json echo "Cleaned up release notes." echo "Pushing to GitHub..." @@ -112,9 +123,3 @@ echo "Pushed to GitHub." echo "Publishing release notes..." hub release create --file "${RELEASE_NOTES_FILE}" "v${NEW_VERSION}" echo "Published release notes." - -echo "Making the tweet..." -npm install --no-save twitter@1.7.1 -cp -v "${WDIR}/scripts/twitter.json" "${TEMPDIR}/${REPOSITORY_NAME}/scripts/" -node ./scripts/tweet.js ${NEW_VERSION} -echo "Made the tweet." diff --git a/scripts/publish/cloudbuild.yaml b/scripts/publish/cloudbuild.yaml index 4113b5875f6..775079346d6 100644 --- a/scripts/publish/cloudbuild.yaml +++ b/scripts/publish/cloudbuild.yaml @@ -1,8 +1,9 @@ steps: # Decrypt the SSH key. - - name: "gcr.io/cloud-builders/gcloud" + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk:slim" args: [ + "gcloud", "kms", "decrypt", "--ciphertext-file=deploy_key.enc", @@ -13,9 +14,10 @@ steps: ] # Decrypt the Twitter credentials. - - name: "gcr.io/cloud-builders/gcloud" + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk:slim" args: [ + "gcloud", "kms", "decrypt", "--ciphertext-file=twitter.json.enc", @@ -26,9 +28,10 @@ steps: ] # Decrypt the npm credentials. - - name: "gcr.io/cloud-builders/gcloud" + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk:slim" args: [ + "gcloud", "kms", "decrypt", "--ciphertext-file=npmrc.enc", @@ -39,9 +42,10 @@ steps: ] # Decrypt the hub (GitHub) credentials. - - name: "gcr.io/cloud-builders/gcloud" + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk:slim" args: [ + "gcloud", "kms", "decrypt", "--ciphertext-file=hub.enc", @@ -94,7 +98,7 @@ steps: # Publish the package. - name: "gcr.io/$PROJECT_ID/package-builder" dir: "${_REPOSITORY_NAME}" - args: ["bash", "./scripts/publish.sh", "${_VERSION}"] + args: ["bash", "./scripts/publish.sh", "${_VERSION}", "${_VSCODE_VERSION}"] env: - "REPOSITORY_ORG=${_REPOSITORY_ORG}" - "REPOSITORY_NAME=${_REPOSITORY_NAME}" @@ -122,6 +126,7 @@ options: substitutions: _VERSION: "" + _VSCODE_VERSION: "" _KEY_RING: "cloud-build-ring" _KEY_NAME: "publish" _REPOSITORY_ORG: "firebase" diff --git a/scripts/publish/run.sh b/scripts/publish/run.sh index 4adb35b7206..2523a31813a 100755 --- a/scripts/publish/run.sh +++ b/scripts/publish/run.sh @@ -6,6 +6,7 @@ printusage() { echo "" echo "Arguments:" echo " version: 'patch', 'minor', or 'major'." + echo " vscode_version: 'patch', 'minor', or 'major'. Defaults to same as version if omitted" } VERSION=$1 @@ -17,6 +18,14 @@ elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]] exit 1 fi +VSCODE_VERSION=$2 +if [[ $VSCODE_VERSION == "" ]]; then + VSCODE_VERSION=$VERSION +elif [[ ! ($VSCODE_VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]]; then + printusage + exit 1 +fi + THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" cd "$THIS_DIR" @@ -24,5 +33,6 @@ cd "$THIS_DIR" gcloud --project fir-tools-builds \ builds \ submit \ - --substitutions=_VERSION=$VERSION \ + --machine-type=e2-highcpu-8 \ + --substitutions=_VERSION=$VERSION,_VSCODE_VERSION=$VSCODE_VERSION \ . \ No newline at end of file diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh new file mode 100755 index 00000000000..65e5ec33f18 --- /dev/null +++ b/scripts/storage-deploy-tests/run.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -e +CWD="$(pwd)" + +source scripts/set-default-credentials.sh + +TARGET_FILE="${COMMIT_SHA}-${CI_JOB_ID}.txt" + +echo "Running in ${CWD}" +echo "Running with node: $(which node)" +echo "Running with npm: $(which npm)" +echo "Running with Application Creds: ${GOOGLE_APPLICATION_CREDENTIALS}" + +echo "Target project: ${FBTOOLS_TARGET_PROJECT}" + +echo "Initializing some variables..." +DATE="$(date)" +NUMBER="$(date '+%Y%m%d%H%M%S')" +echo "Variables initalized..." + +echo "Creating temp directory..." +TEMP_DIR="$(mktemp -d)" +echo "Created temp directory: ${TEMP_DIR}" + +echo "Installing firebase-tools..." +./scripts/clean-install.sh +echo "Installed firebase-tools: $(which firebase)" + +echo "Initializing temp directory..." +cd "${TEMP_DIR}" +cat > "firebase.json" <<- EOM +{ + "storage": { + "rules": "storage.rules" + } +} +EOM +cat > "storage.rules" <<- EOM +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if request.auth!=null && $NUMBER == $NUMBER; + } + } +} +EOM +echo "Initialized temp directory." + +echo "Testing storage deployment..." +firebase deploy --force --only storage --project "${FBTOOLS_TARGET_PROJECT}" +RET_CODE="$?" +test "${RET_CODE}" == "0" || (echo "Expected exit code ${RET_CODE} to equal 0." && false) +echo "Tested storage deployment." + +echo "Updating config for targets..." +cat > "firebase.json" <<- EOM +{ + "storage": [ + { + "target": "storage-target", + "rules": "storage.rules" + } + ] +} +EOM +firebase use --add "${FBTOOLS_TARGET_PROJECT}" +firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" +echo "Updated config for targets." + +echo "Testing storage deployment with invalid target..." +set +e +firebase deploy --force --only storage:storage-invalid-target --project "${FBTOOLS_TARGET_PROJECT}" +RET_CODE="$?" +set -e +test "${RET_CODE}" == "1" || (echo "Expected exit code ${RET_CODE} to equal 1." && false) +echo "Tested storage deployment with invalid target." + +echo "Testing storage deployment with target..." +firebase deploy --force --only storage:storage-target --project "${FBTOOLS_TARGET_PROJECT}" +RET_CODE="$?" +test "${RET_CODE}" == "0" || (echo "Expected exit code ${RET_CODE} to equal 0." && false) +echo "Tested storage deployment with target." \ No newline at end of file diff --git a/scripts/storage-emulator-integration/conformance/env.ts b/scripts/storage-emulator-integration/conformance/env.ts new file mode 100644 index 00000000000..67f8d44a562 --- /dev/null +++ b/scripts/storage-emulator-integration/conformance/env.ts @@ -0,0 +1,177 @@ +import { + readAbsoluteJson, + getProdAccessToken, + getStorageEmulatorHost, + getAuthEmulatorHost, +} from "../utils"; +import * as path from "path"; +import { FrameworkOptions } from "../../integration-helpers/framework"; +import * as fs from "fs"; +import * as http from "http"; +import * as https from "https"; + +// Set these flags to control test behavior. +const TEST_CONFIG = { + // Set this to true to use production servers + // (useful for writing tests against source of truth) + useProductionServers: false, + + // The following two fields MUST be set if useProductionServers == true. + // The paths should be relative to this file. + // + // Follow the instructions here to get your app config: + // https://support.google.com/firebase/answer/7015592#web + prodAppConfigFilePath: "storage-integration-config.json", + // Follow the instructions here to create a service account key file: + // https://firebase.google.com/docs/admin/setup#initialize-sdk + prodServiceAccountKeyFilePath: "service-account-key.json", + + // Name of secondary GCS bucket used in tests that need two buckets. + // When useProductionServers == true, this must be a bucket that + // the prod service account has write access to. + secondTestBucket: "other-bucket", + + // Relative path to the emulator config to use in integration tests. + // Only used when useProductionServers == false. + emulatorConfigFilePath: "../firebase.json", + + // Set this to true to make the headless chrome window used in + // Firebase js sdk integration tests visible. + showBrowser: false, +}; + +// Project id to use when testing against the emulator. Not used in prod +// conformance tests. +const FAKE_FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || "fake-project-id"; + +// Emulators accept fake app configs. This is sufficient for testing against the emulator. +const FAKE_APP_CONFIG = { + apiKey: "fake-api-key", + projectId: `${FAKE_FIREBASE_PROJECT}`, + authDomain: `${FAKE_FIREBASE_PROJECT}.firebaseapp.com`, + storageBucket: `${FAKE_FIREBASE_PROJECT}.appspot.com`, + appId: "fake-app-id", +}; + +function readProdAppConfig() { + const filePath = path.join(__dirname, TEST_CONFIG.prodAppConfigFilePath); + try { + return readAbsoluteJson(filePath); + } catch (error) { + throw new Error(`Cannot read the prod app config file. Please ensure that ${filePath} exists.`); + } +} + +function readEmulatorConfig(): FrameworkOptions { + const filePath = path.join(__dirname, TEST_CONFIG.emulatorConfigFilePath); + try { + return readAbsoluteJson(filePath); + } catch (error) { + throw new Error(`Cannot read the emulator config. Please ensure that ${filePath} exists.`); + } +} + +class ConformanceTestEnvironment { + private _prodAppConfig: any; + private _emulatorConfig: any; + private _prodServiceAccountKeyJson?: any | null; + private _adminAccessToken?: string; + + get useProductionServers() { + return TEST_CONFIG.useProductionServers; + } + + get showBrowser() { + return TEST_CONFIG.showBrowser; + } + get fakeProjectId() { + return FAKE_FIREBASE_PROJECT; + } + + private get prodAppConfig() { + return this._prodAppConfig || (this._prodAppConfig = readProdAppConfig()); + } + + get appConfig() { + return TEST_CONFIG.useProductionServers ? this.prodAppConfig : FAKE_APP_CONFIG; + } + + get emulatorConfig() { + return this._emulatorConfig || (this._emulatorConfig = readEmulatorConfig()); + } + + get storageEmulatorHost() { + return getStorageEmulatorHost(this.emulatorConfig); + } + + get authEmulatorHost() { + return getAuthEmulatorHost(this.emulatorConfig); + } + + get firebaseHost() { + return this.useProductionServers + ? "https://firebasestorage.googleapis.com" + : this.storageEmulatorHost; + } + + get storageHost() { + return this.useProductionServers ? "https://storage.googleapis.com" : this.storageEmulatorHost; + } + + get googleapisHost() { + return this.useProductionServers ? "https://www.googleapis.com" : this.storageEmulatorHost; + } + + get prodServiceAccountKeyJson() { + if (this._prodServiceAccountKeyJson === undefined) { + const filePath = path.join(__dirname, TEST_CONFIG.prodServiceAccountKeyFilePath); + this._prodServiceAccountKeyJson = + TEST_CONFIG.prodServiceAccountKeyFilePath && fs.existsSync(filePath) + ? readAbsoluteJson(filePath) + : null; + } + return this._prodServiceAccountKeyJson; + } + + get requestClient() { + return this.useProductionServers ? https : http; + } + + get adminAccessTokenGetter(): Promise { + if (this._adminAccessToken) { + return Promise.resolve(this._adminAccessToken); + } + const generateAdminAccessToken = this.useProductionServers + ? getProdAccessToken(this.prodServiceAccountKeyJson) + : Promise.resolve("owner"); + return generateAdminAccessToken.then((token) => { + this._adminAccessToken = token; + return token; + }); + } + + get secondTestBucket() { + return TEST_CONFIG.secondTestBucket; + } + + applyEnvVars() { + if (this.useProductionServers) { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join( + __dirname, + TEST_CONFIG.prodServiceAccountKeyFilePath, + ); + } else { + process.env.STORAGE_EMULATOR_HOST = this.storageEmulatorHost; + } + } + + removeEnvVars() { + if (this.useProductionServers) { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + } else { + delete process.env.STORAGE_EMULATOR_HOST; + } + } +} + +export const TEST_ENV = new ConformanceTestEnvironment(); diff --git a/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts new file mode 100644 index 00000000000..8d28b61287e --- /dev/null +++ b/scripts/storage-emulator-integration/conformance/firebase-js-sdk.test.ts @@ -0,0 +1,715 @@ +import { Bucket } from "@google-cloud/storage"; +import { expect } from "chai"; +import firebasePkg from "firebase/compat/app"; +import { applicationDefault, cert, deleteApp, getApp, initializeApp } from "firebase-admin/app"; +import { getStorage } from "firebase-admin/storage"; +import * as fs from "fs"; +import * as puppeteer from "puppeteer"; +import { TEST_ENV } from "./env"; +import { IMAGE_FILE_BASE64 } from "../../../src/emulator/testing/fixtures"; +import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; +import { + createRandomFile, + EMULATORS_SHUTDOWN_DELAY_MS, + resetStorageEmulator, + SMALL_FILE_SIZE, + TEST_SETUP_TIMEOUT, + getTmpDir, +} from "../utils"; + +const TEST_FILE_NAME = "testing/storage_ref/testFile"; + +// Test case that should only run when targeting the emulator. +// Example use: emulatorOnly.it("Local only test case", () => {...}); +const emulatorOnly = { it: TEST_ENV.useProductionServers ? it.skip : it }; + +// This is a 'workaround' to prevent typescript from renaming the import. That +// causes issues when page.evaluate is run with the rename, since the renamed +// values don't exist in the created page. +const firebase = firebasePkg; + +describe("Firebase Storage JavaScript SDK conformance tests", () => { + const storageBucket = TEST_ENV.appConfig.storageBucket; + const expectedFirebaseHost = TEST_ENV.firebaseHost; + + // Temp directory to store generated files. + const tmpDir = getTmpDir(); + const smallFilePath: string = createRandomFile("small_file", SMALL_FILE_SIZE, tmpDir); + const emptyFilePath: string = createRandomFile("empty_file", 0, tmpDir); + + let test: EmulatorEndToEndTest; + let testBucket: Bucket; + let authHeader: { Authorization: string }; + let browser: puppeteer.Browser; + let page: puppeteer.Page; + + async function uploadText( + page: puppeteer.Page, + filename: string, + text: string, + format?: string, + metadata?: firebasePkg.storage.UploadMetadata, + ): Promise { + return page.evaluate( + async (filename, text, format, metadata) => { + try { + const ref = firebase.storage().ref(filename); + const res = await ref.putString(text, format, JSON.parse(metadata)); + return res.state; + } catch (err) { + if (err instanceof Error) { + throw err.message; + } + throw err; + } + }, + filename, + text, + format ?? "raw", + JSON.stringify(metadata ?? {}), + )!; + } + + async function signInToFirebaseAuth(page: puppeteer.Page): Promise { + await page.evaluate(async () => { + await firebase.auth().signInAnonymously(); + }); + } + + async function resetState(): Promise { + if (TEST_ENV.useProductionServers) { + await testBucket.deleteFiles(); + } else { + await resetStorageEmulator(TEST_ENV.storageEmulatorHost); + } + } + + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + TEST_ENV.applyEnvVars(); + if (!TEST_ENV.useProductionServers) { + test = new EmulatorEndToEndTest(TEST_ENV.fakeProjectId, __dirname, TEST_ENV.emulatorConfig); + await test.startEmulators(["--only", "auth,storage"]); + } + + // Init GCS admin SDK. + const credential = TEST_ENV.prodServiceAccountKeyJson + ? cert(TEST_ENV.prodServiceAccountKeyJson) + : applicationDefault(); + initializeApp({ credential }); + testBucket = getStorage().bucket(storageBucket); + authHeader = { Authorization: `Bearer ${await TEST_ENV.adminAccessTokenGetter}` }; + + // Init fake browser page. + browser = await puppeteer.launch({ + headless: !TEST_ENV.showBrowser, + devtools: true, + }); + page = await browser.newPage(); + await page.goto("https://example.com", { waitUntil: "networkidle2" }); + await page.addScriptTag({ + url: "https://www.gstatic.com/firebasejs/9.16.0/firebase-app-compat.js", + }); + await page.addScriptTag({ + url: "https://www.gstatic.com/firebasejs/9.16.0/firebase-auth-compat.js", + }); + await page.addScriptTag({ + url: "https://www.gstatic.com/firebasejs/9.16.0/firebase-storage-compat.js", + }); + + // Init Firebase app in browser context and maybe set emulator host overrides. + console.error("we're going to use this config", TEST_ENV.appConfig); + await page + .evaluate( + (appConfig, useProductionServers, authEmulatorHost, storageEmulatorHost) => { + // throw new Error(window.firebase.toString()); + // if (firebase.apps.length <= 0) { + firebase.initializeApp(appConfig); + // } + if (!useProductionServers) { + firebase.app().auth().useEmulator(authEmulatorHost); + const [storageHost, storagePort] = storageEmulatorHost.split(":"); + firebase.app().storage().useEmulator(storageHost, Number(storagePort)); + } + }, + TEST_ENV.appConfig, + TEST_ENV.useProductionServers, + TEST_ENV.authEmulatorHost, + TEST_ENV.storageEmulatorHost.replace(/^(https?:|)\/\//, ""), + ) + .catch((reason) => { + console.error("*** ", reason); + throw reason; + }); + }); + + beforeEach(async () => { + await resetState(); + }); + + afterEach(async () => { + await page.evaluate(async () => { + await firebase.auth().signOut(); + }); + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + await deleteApp(getApp()); + fs.rmSync(tmpDir, { recursive: true, force: true }); + await page.close(); + await browser.close(); + + TEST_ENV.removeEnvVars(); + if (!TEST_ENV.useProductionServers) { + await test.stopEmulators(); + } + }); + + describe(".ref()", () => { + describe("#putString()", () => { + it("should upload a string", async () => { + await signInToFirebaseAuth(page); + await page.evaluate(async (ref) => { + await firebase.storage().ref(ref).putString("hello world"); + }, TEST_FILE_NAME); + + const downloadUrl = await page.evaluate(async (ref) => { + return await firebase.storage().ref(ref).getDownloadURL(); + }, TEST_FILE_NAME); + + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get(downloadUrl, { headers: authHeader }, (response) => { + const data: any = []; + response + .on("data", (chunk) => data.push(chunk)) + .on("end", () => { + expect(Buffer.concat(data).toString()).to.equal("hello world"); + }) + .on("close", resolve) + .on("error", reject); + }); + }); + }); + }); + + describe("#put()", () => { + it("should upload a file with a really long path name to check for os filename character limit", async () => { + await signInToFirebaseAuth(page); + const uploadState = await uploadText( + page, + `testing/${"long".repeat(180)}image.png`, + IMAGE_FILE_BASE64, + "base64", + ); + + expect(uploadState).to.equal("success"); + }); + + it("should upload replace existing file", async () => { + await uploadText(page, "upload/replace.txt", "some-content"); + await uploadText(page, "upload/replace.txt", "some-other-content"); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const downloadUrl = await page.evaluate(() => { + return firebase.storage().ref("upload/replace.txt").getDownloadURL(); + }); + + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get(downloadUrl, { headers: authHeader }, (response) => { + const data: any = []; + response + .on("data", (chunk) => data.push(chunk)) + .on("end", () => { + expect(Buffer.concat(data).toString()).to.equal("some-other-content"); + }) + .on("close", resolve) + .on("error", reject); + }); + }); + }); + + it("should upload a file using put", async () => { + await signInToFirebaseAuth(page); + const uploadState = await page.evaluate(async (IMAGE_FILE_BASE64) => { + const task = await firebase + .storage() + .ref("testing/image_put.png") + .put(new File([IMAGE_FILE_BASE64], "toUpload.txt")); + return task.state; + }, IMAGE_FILE_BASE64); + + expect(uploadState).to.equal("success"); + }); + + it("should handle uploading empty buffer", async () => { + await signInToFirebaseAuth(page); + const uploadState = await page.evaluate(async () => { + const task = await firebase.storage().ref("testing/empty_file").put(new ArrayBuffer(0)); + return task.state; + }); + + expect(uploadState).to.equal("success"); + }); + + it("should upload a file with custom metadata", async () => { + const uploadState = await page.evaluate(async (IMAGE_FILE_BASE64) => { + const task = await firebase + .storage() + .ref("upload/allowIfContentTypeImage.png") + .put(new File([IMAGE_FILE_BASE64], "toUpload.txt"), { contentType: "image/blah" }); + return task.state; + }, IMAGE_FILE_BASE64); + + expect(uploadState).to.equal("success"); + const [metadata] = await testBucket + .file("upload/allowIfContentTypeImage.png") + .getMetadata(); + expect(metadata.contentType).to.equal("image/blah"); + }); + + it("should return a 403 on rules deny", async () => { + const uploadState = await page.evaluate(async (IMAGE_FILE_BASE64) => { + const _file = new File([IMAGE_FILE_BASE64], "toUpload.txt"); + try { + const task = await firebase + .storage() + .ref("upload/allowIfContentTypeImage.png") + .put(_file, { contentType: "text/plain" }); + return task.state; + } catch (err: any) { + if (err instanceof Error) { + return err.message; + } + throw err; + } + }, IMAGE_FILE_BASE64); + expect(uploadState).to.include("User does not have permission"); + }); + + it("should return a 403 on rules deny when overwriting existing file", async () => { + async function shouldThrowOnUpload() { + try { + return await uploadText(page, "upload/allowIfNoExistingFile.txt", "some-other-content"); + } catch (err: any) { + if (err instanceof Error) { + return err.message; + } + throw err; + } + } + + await uploadText(page, "upload/allowIfNoExistingFile.txt", "some-content"); + + const uploadState = await shouldThrowOnUpload(); + expect(uploadState).to.include("User does not have permission"); + }); + + it("should default to application/octet-stream", async () => { + await signInToFirebaseAuth(page); + const uploadState = await page.evaluate(async (TEST_FILE_NAME) => { + const task = await firebase.storage().ref(TEST_FILE_NAME).put(new ArrayBuffer(8)); + return task.state; + }, TEST_FILE_NAME); + + expect(uploadState).to.equal("success"); + const [metadata] = await testBucket.file(TEST_FILE_NAME).getMetadata(); + expect(metadata.contentType).to.equal("application/octet-stream"); + }); + }); + + describe("#listAll()", () => { + async function uploadFiles(paths: string[], filename = smallFilePath): Promise { + await Promise.all(paths.map((destination) => testBucket.upload(filename, { destination }))); + } + + async function executeListAllAtPath(path: string): Promise<{ + items: string[]; + prefixes: string[]; + }> { + return await page.evaluate(async (path) => { + const list = await firebase.storage().ref(path).listAll(); + return { + prefixes: list.prefixes.map((prefix) => prefix.name), + items: list.items.map((item) => item.name), + }; + }, path); + } + + it("should list all files and prefixes at path", async () => { + await uploadFiles([ + "listAll/some/deeply/nested/directory/item1", + "listAll/item1", + "listAll/item2", + ]); + + const listResult = await executeListAllAtPath("listAll/"); + + expect(listResult).to.deep.equal({ + items: ["item1", "item2"], + prefixes: ["some"], + }); + }); + + it("zero element list array should still be present in response", async () => { + const listResult = await executeListAllAtPath("listAll/"); + + expect(listResult).to.deep.equal({ + prefixes: [], + items: [], + }); + }); + + it("folder placeholder should not be listed under itself", async () => { + await uploadFiles(["listAll/abc/", emptyFilePath]); + + let listResult = await executeListAllAtPath("listAll/"); + + expect(listResult).to.deep.equal({ + prefixes: ["abc"], + items: [], + }); + + listResult = await executeListAllAtPath("listAll/abc/"); + + expect(listResult).to.deep.equal({ + prefixes: [], + items: [], + }); + }); + + it("should not include show invalid prefixes and items", async () => { + await uploadFiles(["listAll//foo", "listAll/bar//", "listAll/baz//qux"], emptyFilePath); + + const listResult = await executeListAllAtPath("listAll/"); + + expect(listResult).to.deep.equal({ + prefixes: ["bar", "baz"], + items: [], // no valid items + }); + }); + }); + + describe("#list()", () => { + async function uploadFiles(paths: string[]): Promise { + await Promise.all( + paths.map((destination) => testBucket.upload(smallFilePath, { destination })), + ); + } + const itemNames = [...Array(10)].map((_, i) => `item#${i}`); + + beforeEach(async () => { + await uploadFiles(itemNames.map((name) => `listAll/${name}`)); + }); + + it("should list only maxResults items with nextPageToken, when maxResults is set", async () => { + const listItems = await page.evaluate(async () => { + const list = await firebase.storage().ref("listAll").list({ + maxResults: 4, + }); + return { + items: list.items.map((item) => item.name), + nextPageToken: list.nextPageToken, + }; + }); + + expect(listItems.items).to.have.lengthOf(4); + expect(itemNames).to.include.members(listItems.items); + expect(listItems.nextPageToken).to.not.be.empty; + }); + + it("should paginate when nextPageToken is provided", async () => { + let responses: string[] = []; + let pageToken = ""; + let pageCount = 0; + + do { + const listResponse = await page.evaluate(async (pageToken) => { + const list = await firebase.storage().ref("listAll").list({ + maxResults: 4, + pageToken, + }); + return { + items: list.items.map((item) => item.name), + nextPageToken: list.nextPageToken ?? "", + }; + }, pageToken); + + responses = [...responses, ...listResponse.items]; + pageToken = listResponse.nextPageToken; + pageCount++; + + if (!listResponse.nextPageToken) { + expect(responses.sort()).to.deep.equal(itemNames); + expect(pageCount).to.be.equal(3); + break; + } + } while (true); + }); + }); + + describe("#getDownloadURL()", () => { + it("returns url pointing to the expected host", async () => { + await testBucket.upload(emptyFilePath, { destination: TEST_FILE_NAME }); + await signInToFirebaseAuth(page); + + const downloadUrl: string = await page.evaluate((filename) => { + return firebase.storage().ref(filename).getDownloadURL(); + }, TEST_FILE_NAME); + expect(downloadUrl).to.contain( + `${expectedFirebaseHost}/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2FtestFile?alt=media&token=`, + ); + }); + + it("serves the right content", async () => { + const contents = Buffer.from("hello world"); + await testBucket.file(TEST_FILE_NAME).save(contents); + await signInToFirebaseAuth(page); + + const downloadUrl = await page.evaluate((filename) => { + return firebase.storage().ref(filename).getDownloadURL(); + }, TEST_FILE_NAME); + + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get(downloadUrl, (response) => { + let data = Buffer.alloc(0); + expect(response.headers["content-disposition"]).to.be.eql( + "attachment; filename*=testFile", + ); + response + .on("data", (chunk) => { + data = Buffer.concat([data, chunk]); + }) + .on("end", () => { + expect(data).to.deep.equal(contents); + }) + .on("close", resolve) + .on("error", reject); + }); + }); + }); + + emulatorOnly.it("serves content successfully when spammed with calls", async function (this) { + this.timeout(10_000); + const NUMBER_OF_FILES = 100; + const allFileNames: string[] = []; + for (let i = 0; i < NUMBER_OF_FILES; i++) { + const fileName = TEST_FILE_NAME.concat(i.toString()); + allFileNames.push(fileName); + await testBucket.upload(smallFilePath, { destination: fileName }); + } + await signInToFirebaseAuth(page); + + const getDownloadUrlPromises: Promise[] = []; + for (const singleFileName of allFileNames) { + getDownloadUrlPromises.push( + page.evaluate((filename) => { + return firebase.storage().ref(filename).getDownloadURL(); + }, singleFileName), + ); + } + const values: string[] = await Promise.all(getDownloadUrlPromises); + + expect(values.length).to.be.equal(NUMBER_OF_FILES); + }); + }); + + describe("#getMetadata()", () => { + it("should return file metadata", async () => { + await testBucket.upload(emptyFilePath, { + destination: TEST_FILE_NAME, + }); + await signInToFirebaseAuth(page); + + const metadata = await page.evaluate(async (filename) => { + return await firebase.storage().ref(filename).getMetadata(); + }, TEST_FILE_NAME); + + expect(Object.keys(metadata)).to.have.same.members([ + "type", + "bucket", + "generation", + "metageneration", + "fullPath", + "name", + "size", + "timeCreated", + "updated", + "md5Hash", + "contentEncoding", + "contentType", + ]); + // Unsure why `type` still exists in practice but not the typing. + // expect(metadata.type).to.be.eql("file"); + expect(metadata.bucket).to.be.eql(storageBucket); + expect(metadata.generation).to.be.a("string"); + // Firebase Storage automatically updates metadata with a download token on data or + // metadata fetch it isn't provided at uplaod time. + expect(metadata.metageneration).to.be.eql("2"); + expect(metadata.fullPath).to.be.eql(TEST_FILE_NAME); + expect(metadata.name).to.be.eql("testFile"); + expect(metadata.size).to.be.eql(0); + expect(metadata.timeCreated).to.be.a("string"); + expect(metadata.updated).to.be.a("string"); + expect(metadata.md5Hash).to.be.a("string"); + expect(metadata.contentEncoding).to.be.eql("identity"); + expect(metadata.contentType).to.be.eql("application/octet-stream"); + }); + }); + + describe("#updateMetadata()", () => { + it("updates metadata successfully", async () => { + await testBucket.upload(emptyFilePath, { destination: TEST_FILE_NAME }); + await signInToFirebaseAuth(page); + + const metadata = await page.evaluate((filename) => { + return firebase + .storage() + .ref(filename) + .updateMetadata({ + contentType: "application/awesome-stream", + customMetadata: { + testable: "true", + }, + }); + }, TEST_FILE_NAME); + + expect(metadata.contentType).to.equal("application/awesome-stream"); + expect(metadata.customMetadata?.testable).to.equal("true"); + }); + + it("shoud allow deletion of settable metadata fields by setting to null", async () => { + await testBucket.upload(emptyFilePath, { + destination: TEST_FILE_NAME, + metadata: { + cacheControl: "hello world", + contentDisposition: "hello world", + contentEncoding: "hello world", + contentLanguage: "en", + contentType: "hello world", + metadata: { key: "value" }, + }, + }); + await signInToFirebaseAuth(page); + + const updatedMetadata = await page.evaluate((filename) => { + return firebase.storage().ref(filename).updateMetadata({ + cacheControl: null, + contentDisposition: null, + contentEncoding: null, + contentLanguage: null, + contentType: null, + customMetadata: null, + }); + }, TEST_FILE_NAME); + expect(Object.keys(updatedMetadata)).to.not.have.members([ + "cacheControl", + "contentDisposition", + "contentLanguage", + "contentType", + "customMetadata", + ]); + expect(updatedMetadata.contentEncoding).to.be.eql("identity"); + }); + + it("should allow deletion of custom metadata by setting to null", async () => { + await testBucket.upload(emptyFilePath, { destination: TEST_FILE_NAME }); + await signInToFirebaseAuth(page); + + const setMetadata = await page.evaluate((filename) => { + return firebase + .storage() + .ref(filename) + .updateMetadata({ + contentType: "text/plain", + customMetadata: { + removeMe: "please", + }, + }); + }, TEST_FILE_NAME); + + expect(setMetadata.customMetadata!.removeMe).to.equal("please"); + + const nulledMetadata = await page.evaluate((filename) => { + return firebase + .storage() + .ref(filename) + .updateMetadata({ + contentType: "text/plain", + customMetadata: { + removeMe: null as any, + }, + }); + }, TEST_FILE_NAME); + + expect(nulledMetadata.customMetadata).to.be.undefined; + }); + + it("throws on non-existent file", async () => { + await signInToFirebaseAuth(page); + const err = await page.evaluate(async () => { + try { + return await firebase + .storage() + .ref("testing/thisFileDoesntExist") + .updateMetadata({ + contentType: "application/awesome-stream", + customMetadata: { + testable: "true", + }, + }); + } catch (_err) { + return _err; + } + }); + + expect(err).to.not.be.empty; + }); + }); + + describe("#delete()", () => { + it("should delete file", async () => { + await testBucket.upload(emptyFilePath, { destination: TEST_FILE_NAME }); + await signInToFirebaseAuth(page); + + await page.evaluate((filename) => { + return firebase.storage().ref(filename).delete(); + }, TEST_FILE_NAME); + + const error = await page.evaluate((filename) => { + return new Promise((resolve) => { + firebase + .storage() + .ref(filename) + .getDownloadURL() + .catch((err) => { + resolve(err.message); + }); + }); + }, TEST_FILE_NAME); + + expect(error).to.contain("does not exist."); + }); + + it("should not delete file when security rule on resource object disallows it", async () => { + await uploadText(page, "delete/disallowIfContentTypeText", "some-content", undefined, { + contentType: "text/plain", + }); + + const error: string = await page.evaluate(async (filename) => { + try { + await firebase.storage().ref(filename).delete(); + return "success"; + } catch (err) { + if (err instanceof Error) { + return err.message; + } + throw err; + } + }, "delete/disallowIfContentTypeText"); + + expect(error).to.contain("does not have permission to access"); + }); + }); + }); +}); diff --git a/scripts/storage-emulator-integration/conformance/firebase.endpoints.test.ts b/scripts/storage-emulator-integration/conformance/firebase.endpoints.test.ts new file mode 100644 index 00000000000..d49af27fa50 --- /dev/null +++ b/scripts/storage-emulator-integration/conformance/firebase.endpoints.test.ts @@ -0,0 +1,719 @@ +import { Bucket } from "@google-cloud/storage"; +import { expect } from "chai"; +import * as admin from "firebase-admin"; +import * as fs from "fs"; +import * as supertest from "supertest"; +import { gunzipSync } from "zlib"; +import { TEST_ENV } from "./env"; +import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; +import { + EMULATORS_SHUTDOWN_DELAY_MS, + resetStorageEmulator, + getTmpDir, + TEST_SETUP_TIMEOUT, + createRandomFile, +} from "../utils"; + +const TEST_FILE_NAME = "testing/storage_ref/testFile"; +const ENCODED_TEST_FILE_NAME = "testing%2Fstorage_ref%2FtestFile"; + +// headers +const uploadStatusHeader = "x-goog-upload-status"; + +// TODO(b/242314185): add more coverage. +describe("Firebase Storage endpoint conformance tests", () => { + // Temp directory to store generated files. + const tmpDir = getTmpDir(); + const smallFilePath = createRandomFile("small_file", 10, tmpDir); + + const firebaseHost = TEST_ENV.firebaseHost; + const storageBucket = TEST_ENV.appConfig.storageBucket; + + let test: EmulatorEndToEndTest; + let testBucket: Bucket; + let authHeader: { Authorization: string }; + + async function resetState(): Promise { + if (TEST_ENV.useProductionServers) { + await testBucket.deleteFiles(); + } else { + await resetStorageEmulator(TEST_ENV.storageEmulatorHost); + } + } + + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + TEST_ENV.applyEnvVars(); + if (!TEST_ENV.useProductionServers) { + test = new EmulatorEndToEndTest(TEST_ENV.fakeProjectId, __dirname, TEST_ENV.emulatorConfig); + await test.startEmulators(["--only", "auth,storage"]); + } + + // Init GCS admin SDK. Used for easier set up/tear down. + const credential = TEST_ENV.prodServiceAccountKeyJson + ? admin.credential.cert(TEST_ENV.prodServiceAccountKeyJson) + : admin.credential.applicationDefault(); + admin.initializeApp({ credential }); + testBucket = admin.storage().bucket(storageBucket); + authHeader = { Authorization: `Bearer ${await TEST_ENV.adminAccessTokenGetter}` }; + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + admin.app().delete(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + + TEST_ENV.removeEnvVars(); + if (!TEST_ENV.useProductionServers) { + await test.stopEmulators(); + } + }); + + beforeEach(async () => { + await resetState(); + }); + + describe("metadata", () => { + it("should set default metadata", async () => { + const fileName = "dir/someFile"; + const encodedFileName = "dir%2FsomeFile"; + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o?name=${fileName}`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + const metadata = await supertest(firebaseHost) + .get(`/v0/b/${storageBucket}/o/${encodedFileName}`) + .set(authHeader) + .expect(200) + .then((res) => res.body); + + expect(Object.keys(metadata)).to.include.members([ + "name", + "bucket", + "generation", + "metageneration", + "timeCreated", + "updated", + "storageClass", + "size", + "md5Hash", + "contentEncoding", + "contentDisposition", + "crc32c", + "etag", + "downloadTokens", + ]); + + expect(metadata.name).to.be.eql(fileName); + expect(metadata.bucket).to.be.eql(storageBucket); + expect(metadata.generation).to.be.a("string"); + expect(metadata.metageneration).to.be.eql("1"); + expect(metadata.timeCreated).to.be.a("string"); + expect(metadata.updated).to.be.a("string"); + expect(metadata.storageClass).to.be.a("string"); + expect(metadata.size).to.be.eql("11"); + expect(metadata.md5Hash).to.be.a("string"); + expect(metadata.contentEncoding).to.be.eql("identity"); + expect(metadata.contentDisposition).to.be.a("string"); + expect(metadata.crc32c).to.be.a("string"); + expect(metadata.etag).to.be.a("string"); + expect(metadata.downloadTokens).to.be.a("string"); + }); + }); + + describe("media upload", () => { + it("should default to media upload if upload type is not provided", async () => { + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o?name=${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + const data = await supertest(firebaseHost) + .get(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?alt=media`) + .set(authHeader) + .expect(200) + .then((res) => res.body); + expect(String(data)).to.eql("hello world"); + }); + }); + + describe("multipart upload", () => { + it("should return an error message when uploading a file with invalid content type", async () => { + const res = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/?name=${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .set({ "x-goog-upload-protocol": "multipart", "content-type": "foo" }) + .send() + .expect(400); + expect(res.text).to.include("Bad content type."); + }); + }); + + describe("resumable upload", () => { + describe("upload", () => { + it("should accept subsequent resumable upload commands without an auth header", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + // No Authorization required in upload + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload", + "X-Goog-Upload-Offset": 0, + }) + .expect(200); + const uploadStatus = await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + // No Authorization required in finalize + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "finalize", + }) + .expect(200) + .then((res) => res.header[uploadStatusHeader]); + + expect(uploadStatus).to.equal("final"); + + await supertest(firebaseHost) + .get(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .expect(200); + }); + + it("should handle resumable uploads with an empty buffer", async () => { + const uploadUrl = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .send({}) + .expect(200) + .then((res) => { + return new URL(res.header["x-goog-upload-url"]); + }); + + const finalizeStatus = await supertest(firebaseHost) + .post(uploadUrl.pathname + uploadUrl.search) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "finalize", + }) + .send({}) + .expect(200) + .then((res) => res.header[uploadStatusHeader]); + expect(finalizeStatus).to.equal("final"); + }); + + it("should return 403 when resumable upload is unauthenticated", async () => { + const testFileName = "disallowSize0"; + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${testFileName}`) + // Authorization missing + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + const uploadStatus = await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + "X-Goog-Upload-Offset": 0, + }) + .expect(403) + .then((res) => res.header[uploadStatusHeader]); + expect(uploadStatus).to.equal("final"); + }); + + it("should return 403 when resumable upload is unauthenticated and finalize is called again", async () => { + const testFileName = "disallowSize0"; + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${testFileName}?uploadType=resumable`) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + "X-Goog-Upload-Offset": 0, + }) + .expect(403); + const uploadStatus = await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "finalize", + }) + .expect(403) + .then((res) => res.header[uploadStatusHeader]); + expect(uploadStatus).to.equal("final"); + }); + + it("should return 200 when resumable upload succeeds and finalize is called again", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?uploadType=resumable`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + "X-Goog-Upload-Offset": 0, + }) + .expect(200); + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "finalize", + }) + .expect(200); + }); + + it("should return 400 both times when finalize is called on cancelled upload", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?uploadType=resumable`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "cancel", + "X-Goog-Upload-Offset": 0, + }) + .expect(200); + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "finalize", + }) + .expect(400); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "finalize", + }) + .expect(400); + }); + + it("should handle resumable uploads with without upload protocol set", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => { + return new URL(res.header["x-goog-upload-url"]); + }); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Command": "upload", + "X-Goog-Upload-Offset": 0, + }) + .expect(200); + const uploadStatus = await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Command": "finalize", + }) + .expect(200) + .then((res) => res.header[uploadStatusHeader]); + + expect(uploadStatus).to.equal("final"); + + await supertest(firebaseHost) + .get(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .expect(200); + }); + }); + + describe("cancel", () => { + it("should cancel upload successfully", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "cancel", + }) + .expect(200); + + await supertest(firebaseHost) + .get(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .expect(404); + }); + + it("should return 200 when cancelling already cancelled upload", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "cancel", + }) + .expect(200); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "cancel", + }) + .expect(200); + }); + + it("should return 400 when cancelling finalized resumable upload", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + "X-Goog-Upload-Offset": 0, + }) + .expect(200); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "cancel", + }) + .expect(400); + }); + + it("should return 404 when cancelling non-existent upload", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search.replace(/(upload_id=).*?(&)/, "$1foo$2")) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "cancel", + }) + .expect(404); + }); + }); + }); + + describe("gzip", () => { + it("should serve gunzipped file by default", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${firebaseHost}/v0/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.undefined; + expect(res.headers["content-length"]).to.be.undefined; + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + }, + ); + }); + }); + + it("should serve gzipped file if Accept-Encoding header allows", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${firebaseHost}/v0/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader, "Accept-Encoding": "gzip" } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.eql("gzip"); + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.not.be.eql(contents); + const decompressed = gunzipSync(responseBody); + expect(decompressed).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + }, + ); + }); + }); + }); + + describe("upload status", () => { + it("should update the status to active after an upload is started", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + const queryUploadStatus = await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "query", + }) + .expect(200) + .then((res) => res.header[uploadStatusHeader]); + expect(queryUploadStatus).to.equal("active"); + }); + it("should update the status to cancelled after an upload is cancelled", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "cancel", + }) + .expect(200); + const queryUploadStatus = await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "query", + }) + .expect(200) + .then((res) => res.header[uploadStatusHeader]); + expect(queryUploadStatus).to.equal("cancelled"); + }); + it("should update the status to final after an upload is finalized", async () => { + const uploadURL = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload", + "X-Goog-Upload-Offset": 0, + }) + .expect(200); + await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "finalize", + }) + .expect(200) + .then((res) => res.header[uploadStatusHeader]); + const queryUploadStatus = await supertest(firebaseHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "query", + }) + .expect(200) + .then((res) => res.header[uploadStatusHeader]); + expect(queryUploadStatus).to.equal("final"); + }); + }); + + describe("tokens", () => { + beforeEach(async () => { + await testBucket.upload(smallFilePath, { destination: TEST_FILE_NAME }); + }); + + it("should generate new token on create_token", async () => { + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?create_token=true`) + .set(authHeader) + .expect(200) + .then((res) => { + const metadata = res.body; + expect(metadata.downloadTokens.split(",").length).to.deep.equal(1); + }); + }); + + it("should return a 400 if create_token value is invalid", async () => { + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?create_token=someNonTrueParam`) + .set(authHeader) + .expect(400); + }); + + it("should return a 403 for create_token if auth header is invalid", async () => { + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?create_token=true`) + .set({ Authorization: "Bearer somethingElse" }) + .expect(403); + }); + + it("should delete a download token", async () => { + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?create_token=true`) + .set(authHeader) + .expect(200); + const tokens = await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?create_token=true`) + .set(authHeader) + .expect(200) + .then((res) => res.body.downloadTokens.split(",")); + // delete the newly added token + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?delete_token=${tokens[0]}`) + .set(authHeader) + .expect(200) + .then((res) => { + const metadata = res.body; + expect(metadata.downloadTokens.split(",")).to.deep.equal([tokens[1]]); + }); + }); + + it("should regenerate a new token if the last remaining one is deleted", async () => { + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?create_token=true`) + .set(authHeader) + .expect(200); + const token = await supertest(firebaseHost) + .get(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .expect(200) + .then((res) => res.body.downloadTokens); + + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?delete_token=${token}`) + .set(authHeader) + .expect(200) + .then((res) => { + const metadata = res.body; + expect(metadata.downloadTokens.split(",").length).to.deep.equal(1); + expect(metadata.downloadTokens.split(",")).to.not.deep.equal([token]); + }); + }); + + it("should return a 403 for delete_token if auth header is invalid", async () => { + await supertest(firebaseHost) + .post(`/v0/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?delete_token=someToken`) + .set({ Authorization: "Bearer somethingElse" }) + .expect(403); + }); + }); +}); diff --git a/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts b/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts new file mode 100644 index 00000000000..64315dde57a --- /dev/null +++ b/scripts/storage-emulator-integration/conformance/gcs-js-sdk.test.ts @@ -0,0 +1,984 @@ +import { Bucket, CopyOptions } from "@google-cloud/storage"; +import { expect } from "chai"; +import * as admin from "firebase-admin"; +import * as fs from "fs"; +import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; +import { TEST_ENV } from "./env"; +import { + createRandomFile, + EMULATORS_SHUTDOWN_DELAY_MS, + resetStorageEmulator, + SMALL_FILE_SIZE, + TEST_SETUP_TIMEOUT, + getTmpDir, +} from "../utils"; +import { gunzipSync } from "zlib"; + +// Test case that should only run when targeting the emulator. +// Example use: emulatorOnly.it("Local only test case", () => {...}); +const emulatorOnly = { it: TEST_ENV.useProductionServers ? it.skip : it }; + +describe("GCS Javascript SDK conformance tests", () => { + // Temp directory to store generated files. + const tmpDir = getTmpDir(); + const smallFilePath: string = createRandomFile("small_file", SMALL_FILE_SIZE, tmpDir); + const emptyFilePath: string = createRandomFile("empty_file", 0, tmpDir); + + const storageBucket = TEST_ENV.appConfig.storageBucket; + const otherStorageBucket = TEST_ENV.secondTestBucket; + const storageHost = TEST_ENV.storageHost; + const googleapisHost = TEST_ENV.googleapisHost; + + let test: EmulatorEndToEndTest; + let testBucket: Bucket; + let otherTestBucket: Bucket; + let authHeader: { Authorization: string }; + + async function resetState(): Promise { + if (TEST_ENV.useProductionServers) { + await testBucket.deleteFiles(); + } else { + await resetStorageEmulator(TEST_ENV.storageEmulatorHost); + } + } + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + TEST_ENV.applyEnvVars(); + if (!TEST_ENV.useProductionServers) { + test = new EmulatorEndToEndTest(TEST_ENV.fakeProjectId, __dirname, TEST_ENV.emulatorConfig); + await test.startEmulators(["--only", "storage"]); + } + + // Init GCS admin SDK. + const credential = TEST_ENV.prodServiceAccountKeyJson + ? admin.credential.cert(TEST_ENV.prodServiceAccountKeyJson) + : admin.credential.applicationDefault(); + admin.initializeApp({ credential }); + testBucket = admin.storage().bucket(storageBucket); + otherTestBucket = admin.storage().bucket(otherStorageBucket); + authHeader = { Authorization: `Bearer ${await TEST_ENV.adminAccessTokenGetter}` }; + }); + + beforeEach(async () => { + await resetState(); + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + admin.app().delete(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + + TEST_ENV.removeEnvVars(); + if (!TEST_ENV.useProductionServers) { + await test.stopEmulators(); + } + }); + + describe(".bucket()", () => { + describe("#upload()", () => { + it("should handle non-resumable uploads", async () => { + await testBucket.upload(smallFilePath, { + resumable: false, + }); + // Doesn't require an assertion, will throw on failure + }); + + it("should replace existing file on upload", async () => { + const path = "replace.txt"; + const content1 = createRandomFile("small_content_1", 10, tmpDir); + const content2 = createRandomFile("small_content_2", 10, tmpDir); + const file = testBucket.file(path); + + await testBucket.upload(content1, { + destination: path, + }); + + const [readContent1] = await file.download(); + + expect(readContent1).to.deep.equal(fs.readFileSync(content1)); + + await testBucket.upload(content2, { + destination: path, + }); + + const [readContent2] = await file.download(); + expect(readContent2).to.deep.equal(fs.readFileSync(content2)); + + fs.unlinkSync(content1); + fs.unlinkSync(content2); + }); + + it("should upload with provided metadata", async () => { + const metadata = { + contentDisposition: "attachment", + cacheControl: "private,max-age=30", + contentLanguage: "en", + metadata: { foo: "bar" }, + }; + const [, fileMetadata] = await testBucket.upload(smallFilePath, { + resumable: false, + metadata, + }); + + expect(fileMetadata).to.deep.include(metadata); + }); + + it("should upload with proper content type", async () => { + const jpgFile = createRandomFile("small_file.jpg", SMALL_FILE_SIZE, tmpDir); + const [, fileMetadata] = await testBucket.upload(jpgFile); + + expect(fileMetadata.contentType).to.equal("image/jpeg"); + }); + + it("should handle firebaseStorageDownloadTokens", async () => { + const testFileName = "public/file"; + await testBucket.upload(smallFilePath, { + destination: testFileName, + metadata: {}, + }); + + const file = testBucket.file(testFileName); + const incomingMetadata = { + metadata: { + firebaseStorageDownloadTokens: "myFirstToken,mySecondToken", + }, + }; + await file.setMetadata(incomingMetadata); + + const [storedMetadata] = await file.getMetadata(); + expect(storedMetadata.metadata.firebaseStorageDownloadTokens).to.deep.equal( + incomingMetadata.metadata.firebaseStorageDownloadTokens, + ); + }); + + it("should be able to upload file named 'prefix/file.txt' when file named 'prefix' already exists", async () => { + await testBucket.upload(smallFilePath, { + destination: "prefix", + }); + await testBucket.upload(smallFilePath, { + destination: "prefix/file.txt", + }); + }); + + it("should be able to upload file named 'prefix' when file named 'prefix/file.txt' already exists", async () => { + await testBucket.upload(smallFilePath, { + destination: "prefix/file.txt", + }); + await testBucket.upload(smallFilePath, { + destination: "prefix", + }); + }); + }); + + describe("#getFiles()", () => { + const TESTING_FILE = "testing/shoveler.svg"; + const PREFIX_FILE = "prefix"; + const PREFIX_1_FILE = PREFIX_FILE + "/1.txt"; + const PREFIX_2_FILE = PREFIX_FILE + "/2.txt"; + const PREFIX_SUB_DIRECTORY_FILE = PREFIX_FILE + "/dir/file.txt"; + + beforeEach(async () => { + await Promise.all( + [TESTING_FILE, PREFIX_FILE, PREFIX_1_FILE, PREFIX_2_FILE, PREFIX_SUB_DIRECTORY_FILE].map( + async (f) => { + await testBucket.upload(smallFilePath, { + destination: f, + }); + }, + ), + ); + }); + + it("should list all files in bucket", async () => { + // This is only test that uses autoPagination as the other tests look at the prefixes response + const [files] = await testBucket.getFiles(); + + expect(files.map((file) => file.name)).to.deep.equal([ + PREFIX_FILE, + PREFIX_1_FILE, + PREFIX_2_FILE, + PREFIX_SUB_DIRECTORY_FILE, + TESTING_FILE, + ]); + }); + + it("should list all files in bucket using maxResults and pageToken", async () => { + const [files1, , { nextPageToken: nextPageToken1 }] = await testBucket.getFiles({ + maxResults: 3, + }); + + expect(nextPageToken1).to.be.a("string").and.not.empty; + expect(files1.map((file) => file.name)).to.deep.equal([ + PREFIX_FILE, + PREFIX_1_FILE, + PREFIX_2_FILE, + ]); + + const [files2, , { nextPageToken: nextPageToken2 }] = await testBucket.getFiles({ + maxResults: 3, + pageToken: nextPageToken1, + }); + + expect(nextPageToken2).to.be.undefined; + expect(files2.map((file) => file.name)).to.deep.equal([ + PREFIX_SUB_DIRECTORY_FILE, + TESTING_FILE, + ]); + }); + + it("should list files with prefix", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "prefix", + }); + + expect(prefixes).to.be.undefined; + expect(files.map((file) => file.name)).to.deep.equal([ + PREFIX_FILE, + PREFIX_1_FILE, + PREFIX_2_FILE, + PREFIX_SUB_DIRECTORY_FILE, + ]); + }); + + it("should list files using common delimiter", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + delimiter: "/", + }); + + expect(prefixes).to.be.deep.equal(["prefix/", "testing/"]); + expect(files.map((file) => file.name)).to.deep.equal([PREFIX_FILE]); + }); + + it("should list files using other delimiter", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + delimiter: "dir", + }); + + expect(prefixes).to.be.deep.equal(["prefix/dir"]); + expect(files.map((file) => file.name)).to.deep.equal([ + PREFIX_FILE, + PREFIX_1_FILE, + PREFIX_2_FILE, + TESTING_FILE, + ]); + }); + + it("should list files using same prefix and delimiter of p", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "p", + delimiter: "p", + }); + + expect(prefixes).to.be.undefined; + expect(files.map((file) => file.name)).to.deep.equal([ + PREFIX_FILE, + PREFIX_1_FILE, + PREFIX_2_FILE, + PREFIX_SUB_DIRECTORY_FILE, + ]); + }); + + it("should list files using same prefix and delimiter of t", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "t", + delimiter: "t", + }); + + expect(prefixes).to.be.deep.equal(["test"]); + expect(files.map((file) => file.name)).to.be.empty; + }); + + it("should list files using prefix=p and delimiter=t", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "p", + delimiter: "t", + }); + + expect(prefixes).to.be.deep.equal(["prefix/1.t", "prefix/2.t", "prefix/dir/file.t"]); + expect(files.map((file) => file.name)).to.deep.equal([PREFIX_FILE]); + }); + + it("should list files in sub-directory (using prefix and delimiter)", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "prefix/", + delimiter: "/", + }); + + expect(prefixes).to.be.deep.equal(["prefix/dir/"]); + expect(files.map((file) => file.name)).to.deep.equal([PREFIX_1_FILE, PREFIX_2_FILE]); + }); + + it("should list files in sub-directory (using prefix)", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "prefix/", + }); + + expect(prefixes).to.be.undefined; + expect(files.map((file) => file.name)).to.deep.equal([ + PREFIX_1_FILE, + PREFIX_2_FILE, + PREFIX_SUB_DIRECTORY_FILE, + ]); + }); + + it("should list files in sub-directory (using directory)", async () => { + const res = await testBucket.getFiles({ + autoPaginate: false, + prefix: "testing/", + }); + const [files, , { prefixes }] = res; + + expect(prefixes).to.be.undefined; + expect(files.map((file) => file.name)).to.deep.equal([TESTING_FILE]); + }); + + it("should list no files for unused prefix", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "blah/", + }); + + expect(prefixes).to.be.undefined; + expect(files).to.be.empty; + }); + + it("should list files using prefix=pref and delimiter=i", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "pref", + delimiter: "i", + }); + + expect(prefixes).to.be.deep.equal(["prefi"]); + expect(files).to.be.empty; + }); + + it("should list files using prefix=prefi and delimiter=i", async () => { + const [files, , { prefixes }] = await testBucket.getFiles({ + autoPaginate: false, + prefix: "prefi", + delimiter: "i", + }); + + expect(prefixes).to.be.deep.equal(["prefix/di"]); + expect(files.map((file) => file.name)).to.deep.equal([ + PREFIX_FILE, + PREFIX_1_FILE, + PREFIX_2_FILE, + ]); + }); + }); + }); + + describe(".file()", () => { + describe("#save()", () => { + it("should save", async () => { + const contents = Buffer.from("hello world"); + + const file = testBucket.file("gzippedFile"); + await file.save(contents, { contentType: "text/plain" }); + + expect(file.metadata.contentType).to.be.eql("text/plain"); + const [downloadedContents] = await file.download(); + expect(downloadedContents).to.be.eql(contents); + }); + + it("should handle gzipped uploads", async () => { + const file = testBucket.file("gzippedFile"); + await file.save("hello world", { gzip: true }); + + expect(file.metadata.contentEncoding).to.be.eql("gzip"); + }); + }); + + describe("#exists()", () => { + it("should return false for a file that does not exist", async () => { + // Ensure that the file exists on the bucket before deleting it + const [exists] = await testBucket.file("no-file").exists(); + expect(exists).to.equal(false); + }); + + it("should return true for a file that exists", async () => { + // We use a nested path to ensure that we don't need to decode + // the objectId in the gcloud emulator API + const bucketFilePath = "file/to/exists"; + await testBucket.upload(smallFilePath, { + destination: bucketFilePath, + }); + + const [exists] = await testBucket.file(bucketFilePath).exists(); + expect(exists).to.equal(true); + }); + + it("should return false when called on a directory containing files", async () => { + // We use a nested path to ensure that we don't need to decode + // the objectId in the gcloud emulator API + const path = "file/to"; + const bucketFilePath = path + "/exists"; + await testBucket.upload(smallFilePath, { + destination: bucketFilePath, + }); + + const [exists] = await testBucket.file(path).exists(); + expect(exists).to.equal(false); + }); + }); + + describe("#delete()", () => { + it("should delete a file from the bucket", async () => { + // We use a nested path to ensure that we don't need to decode + // the objectId in the gcloud emulator API + const bucketFilePath = "file/to/delete"; + await testBucket.upload(smallFilePath, { + destination: bucketFilePath, + }); + + // Get a reference to the uploaded file + const toDeleteFile = testBucket.file(bucketFilePath); + + // Ensure that the file exists on the bucket before deleting it + const [existsBefore] = await toDeleteFile.exists(); + expect(existsBefore).to.equal(true); + + // Delete it + await toDeleteFile.delete(); + // Ensure that it doesn't exist anymore on the bucket + const [existsAfter] = await toDeleteFile.exists(); + expect(existsAfter).to.equal(false); + }); + + it("should throw 404 object error for file not found", async () => { + await expect(testBucket.file("blah").delete()) + .to.be.eventually.rejectedWith(`No such object: ${storageBucket}/blah`) + .and.nested.include({ + code: 404, + "errors[0].reason": "notFound", + }); + }); + }); + + describe("#download()", () => { + it("should return the content of the file", async () => { + await testBucket.upload(smallFilePath); + const [downloadContent] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .download(); + + const actualContent = fs.readFileSync(smallFilePath); + expect(downloadContent).to.deep.equal(actualContent); + }); + + it("should return partial content of the file", async () => { + await testBucket.upload(smallFilePath); + const [downloadContent] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + // Request 10 bytes (range requests are inclusive) + .download({ start: 10, end: 19 }); + + const actualContent = fs.readFileSync(smallFilePath).slice(10, 20); + expect(downloadContent).to.have.lengthOf(10).and.deep.equal(actualContent); + }); + + it("should throw 404 error for file not found", async () => { + const err = (await expect(testBucket.file("blah").download()).to.be.eventually.rejectedWith( + `No such object: ${storageBucket}/blah`, + )) as Error; + + expect(err).to.have.property("code", 404); + expect(err).not.have.nested.property("errors[0]"); + }); + + it("should decompress gzipped file", async () => { + const contents = Buffer.from("hello world"); + + const file = testBucket.file("gzippedFile"); + await file.save(contents, { gzip: true }); + + const [downloadedContents] = await file.download(); + expect(downloadedContents).to.be.eql(contents); + }); + + it("should serve gzipped file if decompress option specified", async () => { + const contents = Buffer.from("hello world"); + + const file = testBucket.file("gzippedFile"); + await file.save(contents, { gzip: true }); + + const [downloadedContents] = await file.download({ decompress: false }); + expect(downloadedContents).to.not.be.eql(contents); + + const ungzippedContents = gunzipSync(downloadedContents); + expect(ungzippedContents).to.be.eql(contents); + }); + }); + + describe("#copy()", () => { + const COPY_DESTINATION_FILENAME = "copied_file"; + + it("should copy the file", async () => { + await testBucket.upload(smallFilePath); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + const [, resp] = await testBucket.file(smallFilePath.split("/").slice(-1)[0]).copy(file); + + expect(resp) + .to.have.all.keys(["kind", "totalBytesRewritten", "objectSize", "done", "resource"]) + .and.include({ + kind: "storage#rewriteResponse", + totalBytesRewritten: String(SMALL_FILE_SIZE), + objectSize: String(SMALL_FILE_SIZE), + done: true, + }); + + const [copiedContent] = await file.download(); + + const actualContent = fs.readFileSync(smallFilePath); + expect(copiedContent).to.deep.equal(actualContent); + }); + + it("should copy the file to a different bucket", async () => { + await testBucket.upload(smallFilePath); + + const file = otherTestBucket.file(COPY_DESTINATION_FILENAME); + const [, { resource: metadata }] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .copy(file); + + expect(metadata).to.have.property("bucket", otherStorageBucket); + + const [copiedContent] = await file.download(); + + const actualContent = fs.readFileSync(smallFilePath); + expect(copiedContent).to.deep.equal(actualContent); + }); + + it("should return the metadata of the destination file", async () => { + await testBucket.upload(smallFilePath); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + const [, { resource: actualMetadata }] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .copy(file); + + const [expectedMetadata] = await file.getMetadata(); + delete actualMetadata["owner"]; + expect(actualMetadata).to.deep.equal(expectedMetadata); + }); + + it("should copy the file preserving the original metadata", async () => { + const [, source] = await testBucket.upload(smallFilePath, { + metadata: { + contentType: "image/jpg", + cacheControl: "private,no-store", + metadata: { + hello: "world", + }, + }, + }); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + await testBucket.file(smallFilePath.split("/").slice(-1)[0]).copy(file); + + const [metadata] = await file.getMetadata(); + + expect(metadata).to.have.all.keys(source).and.deep.include({ + bucket: source.bucket, + crc32c: source.crc32c, + cacheControl: source.cacheControl, + metadata: source.metadata, + }); + }); + + it("should copy the file and overwrite with the provided custom metadata", async () => { + const [, source] = await testBucket.upload(smallFilePath, { + metadata: { + cacheControl: "private,no-store", + metadata: { + hello: "world", + }, + }, + }); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + const metadata = { foo: "bar" }; + const cacheControl = "private,max-age=10,immutable"; + // Types for CopyOptions are wrong (@google-cloud/storage sub-dependency needs + // update to include https://github.com/googleapis/nodejs-storage/pull/1406 + // and https://github.com/googleapis/nodejs-storage/pull/1426) + const copyOpts: CopyOptions & { [key: string]: unknown } = { + metadata, + cacheControl, + }; + const [, { resource: metadata1 }] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .copy(file, copyOpts); + + delete metadata1["owner"]; + expect(metadata1).to.deep.include({ + bucket: source.bucket, + crc32c: source.crc32c, + metadata, + cacheControl, + }); + + // Also double check with a new metadata fetch + const [metadata2] = await file.getMetadata(); + expect(metadata2).to.deep.equal(metadata1); + }); + + it("should set null custom metadata values to empty strings", async () => { + const [, source] = await testBucket.upload(smallFilePath); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + const metadata = { foo: "bar", nullMetadata: null }; + const cacheControl = "private,max-age=10,immutable"; + // Types for CopyOptions are wrong (@google-cloud/storage sub-dependency needs + // update to include https://github.com/googleapis/nodejs-storage/pull/1406 + // and https://github.com/googleapis/nodejs-storage/pull/1426) + const copyOpts: CopyOptions & { [key: string]: unknown } = { + metadata, + cacheControl, + }; + const [, { resource: metadata1 }] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .copy(file, copyOpts); + + delete metadata1["owner"]; + expect(metadata1).to.deep.include({ + bucket: source.bucket, + crc32c: source.crc32c, + metadata: { + foo: "bar", + // Sets null metadata values to empty strings + nullMetadata: "", + }, + cacheControl, + }); + + // Also double check with a new metadata fetch + const [metadata2] = await file.getMetadata(); + expect(metadata2).to.deep.equal(metadata1); + }); + + it("should preserve firebaseStorageDownloadTokens", async () => { + const firebaseStorageDownloadTokens = "token1,token2"; + await testBucket.upload(smallFilePath, { + metadata: { + metadata: { + firebaseStorageDownloadTokens, + }, + }, + }); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + const [, { resource: metadata }] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .copy(file); + + expect(metadata).to.deep.include({ + metadata: { + firebaseStorageDownloadTokens, + }, + }); + }); + + it("should remove firebaseStorageDownloadTokens when overwriting custom metadata", async () => { + await testBucket.upload(smallFilePath, { + metadata: { + metadata: { + firebaseStorageDownloadTokens: "token1,token2", + }, + }, + }); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + const metadata = { foo: "bar" }; + // Types for CopyOptions are wrong (@google-cloud/storage sub-dependency needs + // update to include https://github.com/googleapis/nodejs-storage/pull/1406 + // and https://github.com/googleapis/nodejs-storage/pull/1426) + const copyOpts: CopyOptions & { [key: string]: unknown } = { + metadata, + }; + const [, { resource: metadataOut }] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .copy(file, copyOpts); + + expect(metadataOut).to.deep.include({ metadata }); + }); + + emulatorOnly.it("should not support the use of a rewriteToken", async () => { + await testBucket.upload(smallFilePath); + + const file = testBucket.file(COPY_DESTINATION_FILENAME); + await expect( + testBucket.file(smallFilePath.split("/").slice(-1)[0]).copy(file, { token: "foo-bar" }), + ).to.eventually.be.rejected.and.have.property("code", 501); + }); + }); + + describe("#makePublic()", () => { + it("should no-op", async () => { + const destination = "a/b"; + await testBucket.upload(smallFilePath, { destination }); + const [aclMetadata] = await testBucket.file(destination).makePublic(); + + const generation = aclMetadata.generation; + delete aclMetadata.generation; + + expect(aclMetadata.kind).to.be.eql("storage#objectAccessControl"); + expect(aclMetadata.object).to.be.eql(destination); + expect(aclMetadata.id).to.be.eql( + `${testBucket.name}/${destination}/${generation}/allUsers`, + ); + expect(aclMetadata.selfLink).to.be.eql( + `${googleapisHost}/storage/v1/b/${testBucket.name}/o/${encodeURIComponent( + destination, + )}/acl/allUsers`, + ); + expect(aclMetadata.bucket).to.be.eql(testBucket.name); + expect(aclMetadata.entity).to.be.eql("allUsers"); + expect(aclMetadata.role).to.be.eql("READER"); + expect(aclMetadata.etag).to.be.a("string"); + }); + + it("should not interfere with downloading of bytes via public URL", async () => { + const destination = "a/b"; + await testBucket.upload(smallFilePath, { destination }); + await testBucket.file(destination).makePublic(); + + const publicLink = `${storageHost}/${testBucket.name}/${destination}`; + + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get(publicLink, {}, (response) => { + const data: any = []; + response + .on("data", (chunk) => data.push(chunk)) + .on("end", () => { + expect(Buffer.concat(data).length).to.equal(SMALL_FILE_SIZE); + }) + .on("close", resolve) + .on("error", reject); + }); + }); + }); + }); + + describe("#getMetadata()", () => { + it("should throw on non-existing file", async () => { + let err: any; + await testBucket + .file(smallFilePath) + .getMetadata() + .catch((_err) => { + err = _err; + }); + + expect(err).to.not.be.empty; + }); + + it("should return generated metadata for new upload", async () => { + const fileName = "test_file"; + await testBucket.upload(emptyFilePath, { destination: fileName }); + + const [metadata] = await testBucket.file(fileName).getMetadata(); + + expect(Object.keys(metadata)).to.have.same.members([ + "kind", + "id", + "selfLink", + "mediaLink", + "name", + "bucket", + "generation", + "metageneration", + "contentType", + "storageClass", + "size", + "md5Hash", + "crc32c", + "etag", + "timeCreated", + "updated", + "timeStorageClassUpdated", + ]); + expect(metadata.kind).to.be.eql("storage#object"); + expect(metadata.id).to.be.include(`${storageBucket}/${fileName}`); + expect(metadata.selfLink).to.include( + `${googleapisHost}/storage/v1/b/${storageBucket}/o/${fileName}`, + ); + expect(metadata.mediaLink).to.include( + `${storageHost}/download/storage/v1/b/${storageBucket}/o/${fileName}`, + ); + expect(metadata.mediaLink).to.include(`alt=media`); + expect(metadata.name).to.be.eql(fileName); + expect(metadata.bucket).to.be.eql(storageBucket); + expect(metadata.generation).to.be.a("string"); + expect(metadata.metageneration).to.be.eql("1"); + expect(metadata.contentType).to.be.eql("application/octet-stream"); + expect(metadata.storageClass).to.be.a("string"); + expect(metadata.size).to.be.eql("0"); + expect(metadata.md5Hash).to.be.a("string"); + expect(metadata.crc32c).to.be.a("string"); + expect(metadata.etag).to.be.a("string"); + expect(metadata.timeCreated).to.be.a("string"); + expect(metadata.updated).to.be.a("string"); + expect(metadata.timeStorageClassUpdated).to.be.a("string"); + }); + + it("should return a functional media link", async () => { + await testBucket.upload(smallFilePath); + const [{ mediaLink }] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .getMetadata(); + + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get(mediaLink, { headers: authHeader }, (response) => { + const data: any = []; + response + .on("data", (chunk) => data.push(chunk)) + .on("end", () => { + expect(Buffer.concat(data).length).to.equal(SMALL_FILE_SIZE); + }) + .on("close", resolve) + .on("error", reject); + }); + }); + }); + + it("should throw 404 object error for file not found", async () => { + await expect(testBucket.file("blah").getMetadata()) + .to.be.eventually.rejectedWith(`No such object: ${storageBucket}/blah`) + .and.nested.include({ + code: 404, + "errors[0].reason": "notFound", + }); + }); + }); + + describe("#setMetadata()", () => { + it("should throw on non-existing file", async () => { + let err: any; + await testBucket + .file(smallFilePath) + .setMetadata({ contentType: 9000 }) + .catch((_err) => { + err = _err; + }); + + expect(err).to.not.be.empty; + }); + + it("should allow overriding of default metadata", async () => { + await testBucket.upload(smallFilePath); + const [metadata] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .setMetadata({ contentType: "very/fake" }); + + const metadataTypes: { [s: string]: string } = {}; + + for (const key in metadata) { + if (metadata[key]) { + metadataTypes[key] = typeof metadata[key]; + } + } + + expect(metadata.contentType).to.equal("very/fake"); + expect(metadataTypes).to.deep.equal({ + bucket: "string", + contentType: "string", + generation: "string", + md5Hash: "string", + crc32c: "string", + etag: "string", + metageneration: "string", + storageClass: "string", + name: "string", + size: "string", + timeCreated: "string", + updated: "string", + id: "string", + kind: "string", + mediaLink: "string", + selfLink: "string", + timeStorageClassUpdated: "string", + }); + }); + + it("should allow setting of optional metadata", async () => { + await testBucket.upload(smallFilePath); + const [metadata] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .setMetadata({ cacheControl: "no-cache", contentLanguage: "en" }); + + const metadataTypes: { [s: string]: string } = {}; + + for (const key in metadata) { + if (metadata[key]) { + metadataTypes[key] = typeof metadata[key]; + } + } + + expect(metadata.cacheControl).to.equal("no-cache"); + expect(metadata.contentLanguage).to.equal("en"); + }); + + it("should allow fields under .metadata", async () => { + await testBucket.upload(smallFilePath); + const [metadata] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .setMetadata({ metadata: { is_over: "9000" } }); + + expect(metadata.metadata.is_over).to.equal("9000"); + }); + + it("should convert non-string fields under .metadata to strings", async () => { + await testBucket.upload(smallFilePath); + const [metadata] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .setMetadata({ metadata: { booleanValue: true, numberValue: -1 } }); + + expect(metadata.metadata).to.deep.equal({ + booleanValue: "true", + numberValue: "-1", + }); + }); + + it("should remove fields under .metadata when setting to null", async () => { + await testBucket.upload(smallFilePath); + const [metadata1] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .setMetadata({ metadata: { foo: "bar", hello: "world" } }); + + expect(metadata1.metadata).to.deep.equal({ + foo: "bar", + hello: "world", + }); + + const [metadata2] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .setMetadata({ metadata: { foo: null } }); + + expect(metadata2.metadata).to.deep.equal({ + hello: "world", + }); + }); + + it("should ignore any unknown fields", async () => { + await testBucket.upload(smallFilePath); + const [metadata] = await testBucket + .file(smallFilePath.split("/").slice(-1)[0]) + .setMetadata({ nada: "true" }); + + expect(metadata.nada).to.be.undefined; + }); + }); + }); +}); diff --git a/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts new file mode 100644 index 00000000000..ad29d3971be --- /dev/null +++ b/scripts/storage-emulator-integration/conformance/gcs.endpoints.test.ts @@ -0,0 +1,431 @@ +import { Bucket } from "@google-cloud/storage"; +import { expect } from "chai"; +import * as admin from "firebase-admin"; +import * as fs from "fs"; +import * as supertest from "supertest"; +import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; +import { gunzipSync } from "zlib"; +import { TEST_ENV } from "./env"; +import { + EMULATORS_SHUTDOWN_DELAY_MS, + resetStorageEmulator, + TEST_SETUP_TIMEOUT, + getTmpDir, +} from "../utils"; + +// Test case that should only run when targeting the emulator. +// Example use: emulatorOnly.it("Local only test case", () => {...}); +const emulatorOnly = { it: TEST_ENV.useProductionServers ? it.skip : it }; + +const TEST_FILE_NAME = "gcs/testFile"; +const ENCODED_TEST_FILE_NAME = "gcs%2FtestFile"; + +const MULTIPART_REQUEST_BODY = Buffer.from(`--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +content-type: application/json\r +\r +{"name":"${TEST_FILE_NAME}"}\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +content-type: text/plain\r +\r +hello there! +\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r +`); + +// TODO(b/242314185): add more coverage. +describe("GCS endpoint conformance tests", () => { + // Temp directory to store generated files. + const tmpDir = getTmpDir(); + const storageBucket = TEST_ENV.appConfig.storageBucket; + const storageHost = TEST_ENV.storageHost; + const googleapisHost = TEST_ENV.googleapisHost; + + let test: EmulatorEndToEndTest; + let testBucket: Bucket; + let authHeader: { Authorization: string }; + + async function resetState(): Promise { + if (TEST_ENV.useProductionServers) { + await testBucket.deleteFiles(); + } else { + await resetStorageEmulator(TEST_ENV.storageEmulatorHost); + } + } + + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + TEST_ENV.applyEnvVars(); + if (!TEST_ENV.useProductionServers) { + test = new EmulatorEndToEndTest(TEST_ENV.fakeProjectId, __dirname, TEST_ENV.emulatorConfig); + await test.startEmulators(["--only", "storage"]); + } + + // Init GCS admin SDK. Used for easier set up/tear down. + const credential = TEST_ENV.prodServiceAccountKeyJson + ? admin.credential.cert(TEST_ENV.prodServiceAccountKeyJson) + : admin.credential.applicationDefault(); + admin.initializeApp({ credential }); + testBucket = admin.storage().bucket(storageBucket); + authHeader = { Authorization: `Bearer ${await TEST_ENV.adminAccessTokenGetter}` }; + }); + + beforeEach(async () => { + await resetState(); + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + admin.app().delete(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + + TEST_ENV.removeEnvVars(); + if (!TEST_ENV.useProductionServers) { + await test.stopEmulators(); + } + }); + + describe("Headers", () => { + it("should set default response headers on object download", async () => { + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + const res = await supertest(storageHost) + .get(`/storage/v1/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?alt=media`) + .set(authHeader) + .expect(200) + .then((res) => res); + + expect(res.header["content-type"]).to.be.eql("application/octet-stream"); + expect(res.header["content-disposition"]).to.be.eql("attachment; filename*=testFile"); + }); + }); + + describe("Metadata", () => { + it("should set default metadata", async () => { + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + const metadata = await supertest(storageHost) + .get(`/storage/v1/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`) + .set(authHeader) + .expect(200) + .then((res) => res.body); + + expect(Object.keys(metadata)).to.include.members([ + "kind", + "id", + "selfLink", + "mediaLink", + "name", + "bucket", + "generation", + "metageneration", + "storageClass", + "size", + "md5Hash", + "crc32c", + "etag", + "timeCreated", + "updated", + "timeStorageClassUpdated", + ]); + + expect(metadata.kind).to.be.eql("storage#object"); + expect(metadata.id).to.be.include(`${storageBucket}/${TEST_FILE_NAME}`); + expect(metadata.selfLink).to.be.eql( + `${googleapisHost}/storage/v1/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`, + ); + expect(metadata.mediaLink).to.include( + `${storageHost}/download/storage/v1/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}`, + ); + expect(metadata.mediaLink).to.include(`alt=media`); + expect(metadata.name).to.be.eql(TEST_FILE_NAME); + expect(metadata.bucket).to.be.eql(storageBucket); + expect(metadata.generation).to.be.a("string"); + expect(metadata.metageneration).to.be.eql("1"); + expect(metadata.storageClass).to.be.a("string"); + expect(metadata.size).to.be.eql("11"); + expect(metadata.md5Hash).to.be.a("string"); + expect(metadata.crc32c).to.be.a("string"); + expect(metadata.etag).to.be.a("string"); + expect(metadata.timeCreated).to.be.a("string"); + expect(metadata.updated).to.be.a("string"); + expect(metadata.timeStorageClassUpdated).to.be.a("string"); + }); + }); + + describe("Upload protocols", () => { + describe("media upload", () => { + it("should default to media upload if upload type is not provided", async () => { + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + const data = await supertest(storageHost) + .get(`/storage/v1/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?alt=media`) + .set(authHeader) + .expect(200) + .then((res) => res.body); + expect(String(data)).to.eql("hello world"); + }); + }); + + describe("resumable upload", () => { + // GCS emulator resumable upload capabilities are limited and this test asserts its broken state. + emulatorOnly.it("should handle resumable uploads", async () => { + const uploadURL = await supertest(storageHost) + .post( + `/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}&uploadType=resumable`, + ) + .set(authHeader) + .expect(200) + .then((res) => new URL(res.header["location"])); + + const chunk1 = Buffer.from("hello "); + await supertest(storageHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload", + "X-Goog-Upload-Offset": 0, + }) + .send(chunk1) + .expect(200); + + await supertest(storageHost) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + "X-Goog-Upload-Offset": chunk1.byteLength, + }) + .send(Buffer.from("world")); + + const data = await supertest(storageHost) + .get(`/storage/v1/b/${storageBucket}/o/${ENCODED_TEST_FILE_NAME}?alt=media`) + .set(authHeader) + .expect(200) + .then((res) => res.body); + // TODO: Current GCS upload implementation only supports a single `upload` step. + expect(String(data)).to.eql("hello "); + }); + + it("should handle resumable upload with name only in metadata", async () => { + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?uploadType=resumable`) + .set(authHeader) + .send({ name: TEST_FILE_NAME }) + .expect(200); + }); + + it("should return generated custom metadata for new upload", async () => { + const customMetadata = { + contentDisposition: "initialCommit", + contentType: "image/jpg", + name: TEST_FILE_NAME, + }; + + const uploadURL = await supertest(storageHost) + .post( + `/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}&uploadType=resumable`, + ) + .set(authHeader) + .send(customMetadata) + .expect(200) + .then((res) => new URL(res.header["location"])); + + const returnedMetadata = await supertest(storageHost) + .put(uploadURL.pathname + uploadURL.search) + .expect(200) + .then((res) => res.body); + + expect(returnedMetadata.name).to.equal(customMetadata.name); + expect(returnedMetadata.contentType).to.equal(customMetadata.contentType); + expect(returnedMetadata.contentDisposition).to.equal(customMetadata.contentDisposition); + }); + + it("should upload content type properly from x-upload-content-type headers", async () => { + const uploadURL = await supertest(storageHost) + .post( + `/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}&uploadType=resumable`, + ) + .set(authHeader) + .set({ + "x-upload-content-type": "image/png", + }) + .expect(200) + .then((res) => new URL(res.header["location"])); + + const returnedMetadata = await supertest(storageHost) + .put(uploadURL.pathname + uploadURL.search) + .expect(200) + .then((res) => res.body); + + expect(returnedMetadata.contentType).to.equal("image/png"); + }); + }); + + describe("multipart upload", () => { + it("should handle multipart upload with name only in metadata", async () => { + const responseName = await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?uploadType=multipart`) + .set(authHeader) + .set({ + "content-type": "multipart/related; boundary=b1d5b2e3-1845-4338-9400-6ac07ce53c1e", + }) + .send(MULTIPART_REQUEST_BODY) + .expect(200) + .then((res) => res.body.name); + expect(responseName).to.equal(TEST_FILE_NAME); + }); + + it("should respect X-Goog-Upload-Protocol header", async () => { + const responseName = await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o`) + .set(authHeader) + .set({ + "content-type": "multipart/related; boundary=b1d5b2e3-1845-4338-9400-6ac07ce53c1e", + "X-Goog-Upload-Protocol": "multipart", + }) + .send(MULTIPART_REQUEST_BODY) + .expect(200) + .then((res) => res.body.name); + expect(responseName).to.equal(TEST_FILE_NAME); + }); + + it("should return an error message on invalid content type", async () => { + const res = await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .set({ "content-type": "foo" }) + .set({ "X-Goog-Upload-Protocol": "multipart" }) + .send(MULTIPART_REQUEST_BODY) + .expect(400); + + expect(res.text).to.include("Bad content type."); + }); + + it("should upload content type properly from x-upload headers", async () => { + const returnedMetadata = await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?uploadType=multipart`) + .set(authHeader) + .set({ + "content-type": "multipart/related; boundary=b1d5b2e3-1845-4338-9400-6ac07ce53c1e", + }) + .set({ + "x-upload-content-type": "text/plain", + }) + .send(MULTIPART_REQUEST_BODY) + .expect(200) + .then((res) => res.body); + + expect(returnedMetadata.contentType).to.equal("text/plain"); + }); + }); + }); + + describe("Gzip", () => { + it("should serve gunzipped file by default", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${storageHost}/download/storage/v1/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.undefined; + expect(res.headers["content-length"]).to.be.undefined; + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + }, + ); + }); + }); + + it("should serve gzipped file if Accept-Encoding header allows", async () => { + const contents = Buffer.from("hello world"); + const fileName = "gzippedFile"; + const file = testBucket.file(fileName); + await file.save(contents, { + gzip: true, + contentType: "text/plain", + }); + + // Use requestClient since supertest will decompress the response body by default. + await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get( + `${storageHost}/download/storage/v1/b/${storageBucket}/o/${fileName}?alt=media`, + { headers: { ...authHeader, "Accept-Encoding": "gzip" } }, + (res) => { + expect(res.headers["content-encoding"]).to.be.eql("gzip"); + expect(res.headers["content-type"]).to.be.eql("text/plain"); + + let responseBody = Buffer.alloc(0); + res + .on("data", (chunk) => { + responseBody = Buffer.concat([responseBody, chunk]); + }) + .on("end", () => { + expect(responseBody).to.not.be.eql(contents); + const decompressed = gunzipSync(responseBody); + expect(decompressed).to.be.eql(contents); + }) + .on("close", resolve) + .on("error", reject); + }, + ); + }); + }); + }); + + describe("List protocols", () => { + describe("list objects", () => { + // This test is for the '/storage/v1/b/:bucketId/o' url pattern, which is used specifically by the GO Admin SDK + it("should list objects in the provided bucket", async () => { + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + await supertest(storageHost) + .post(`/upload/storage/v1/b/${storageBucket}/o?name=${TEST_FILE_NAME}2`) + .set(authHeader) + .send(Buffer.from("hello world")) + .expect(200); + + const data = await supertest(storageHost) + .get(`/storage/v1/b/${storageBucket}/o`) + .set(authHeader) + .expect(200) + .then((res) => res.body); + expect(data.items.length).to.equal(2); + }); + }); + }); +}); diff --git a/scripts/storage-emulator-integration/conformance/persistence.test.ts b/scripts/storage-emulator-integration/conformance/persistence.test.ts new file mode 100644 index 00000000000..4a3592213dc --- /dev/null +++ b/scripts/storage-emulator-integration/conformance/persistence.test.ts @@ -0,0 +1,143 @@ +import * as puppeteer from "puppeteer"; +import { expect } from "chai"; +import * as admin from "firebase-admin"; +import { Bucket } from "@google-cloud/storage"; +import firebasePkg from "firebase/compat/app"; +import { EmulatorEndToEndTest } from "../../integration-helpers/framework"; +import * as fs from "fs"; +import { TEST_ENV } from "./env"; +import { + createRandomFile, + EMULATORS_SHUTDOWN_DELAY_MS, + resetStorageEmulator, + SMALL_FILE_SIZE, + TEST_SETUP_TIMEOUT, + getTmpDir, +} from "../utils"; + +const TEST_FILE_NAME = "public/testFile"; + +// This is a 'workaround' to prevent typescript from renaming the import. That +// causes issues when page.evaluate is run with the rename, since the renamed +// values don't exist in the created page. +const firebase = firebasePkg; + +// Tests files uploaded from one SDK are available in others. +describe("Storage persistence conformance tests", () => { + // Temp directory to store generated files. + const tmpDir = getTmpDir(); + const smallFilePath: string = createRandomFile("small_file", SMALL_FILE_SIZE, tmpDir); + + let browser: puppeteer.Browser; + let page: puppeteer.Page; + let testBucket: Bucket; + let test: EmulatorEndToEndTest; + + async function resetState(): Promise { + if (TEST_ENV.useProductionServers) { + await testBucket.deleteFiles(); + } else { + await resetStorageEmulator(TEST_ENV.storageEmulatorHost); + } + } + + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + TEST_ENV.applyEnvVars(); + if (!TEST_ENV.useProductionServers) { + test = new EmulatorEndToEndTest(TEST_ENV.fakeProjectId, __dirname, TEST_ENV.emulatorConfig); + await test.startEmulators(["--only", "auth,storage"]); + } + + // Init GCS admin SDK. + const credential = TEST_ENV.prodServiceAccountKeyJson + ? admin.credential.cert(TEST_ENV.prodServiceAccountKeyJson) + : admin.credential.applicationDefault(); + admin.initializeApp({ credential }); + testBucket = admin.storage().bucket(TEST_ENV.appConfig.storageBucket); + + // Init fake browser page. + browser = await puppeteer.launch({ + headless: !TEST_ENV.showBrowser, + devtools: true, + }); + page = await browser.newPage(); + await page.goto("https://example.com", { waitUntil: "networkidle2" }); + await page.addScriptTag({ + url: "https://www.gstatic.com/firebasejs/9.9.1/firebase-app-compat.js", + }); + await page.addScriptTag({ + url: "https://www.gstatic.com/firebasejs/9.9.1/firebase-auth-compat.js", + }); + await page.addScriptTag({ + url: "https://www.gstatic.com/firebasejs/9.9.1/firebase-storage-compat.js", + }); + + // Init Firebase app in browser context and maybe set emulator host overrides. + await page.evaluate( + (appConfig, useProductionServers, authEmulatorHost, storageEmulatorHost) => { + firebase.initializeApp(appConfig); + if (!useProductionServers) { + firebase.auth().useEmulator(authEmulatorHost); + const [storageHost, storagePort] = storageEmulatorHost.split(":") as string[]; + (firebase.storage() as any).useEmulator(storageHost, storagePort); + } + }, + TEST_ENV.appConfig, + TEST_ENV.useProductionServers, + TEST_ENV.authEmulatorHost, + TEST_ENV.storageEmulatorHost.replace(/^(https?:|)\/\//, ""), + ); + }); + + beforeEach(async () => { + await resetState(); + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + admin.app().delete(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + await page.close(); + await browser.close(); + + TEST_ENV.removeEnvVars(); + if (!TEST_ENV.useProductionServers) { + await test.stopEmulators(); + } + }); + + it("gcloud API persisted files should be accessible via Firebase SDK", async () => { + await testBucket.upload(smallFilePath, { destination: TEST_FILE_NAME }); + + const downloadUrl = await page.evaluate((testFileName) => { + return firebase.storage().ref(testFileName).getDownloadURL(); + }, TEST_FILE_NAME); + const data = await new Promise((resolve, reject) => { + TEST_ENV.requestClient.get(downloadUrl, (response) => { + const bufs: any = []; + response + .on("data", (chunk) => bufs.push(chunk)) + .on("end", () => resolve(Buffer.concat(bufs))) + .on("close", resolve) + .on("error", reject); + }); + }); + + expect(data).to.deep.equal(fs.readFileSync(smallFilePath)); + }); + + it("Firebase SDK persisted files should be accessible via gcloud API", async () => { + const fileContent = "some-file-content"; + await page.evaluate( + async (testFileName, fileContent) => { + await firebase.storage().ref(testFileName).putString(fileContent); + }, + TEST_FILE_NAME, + fileContent, + ); + + const [downloadedFileContent] = await testBucket.file(TEST_FILE_NAME).download(); + expect(downloadedFileContent).to.deep.equal(Buffer.from(fileContent)); + }); +}); diff --git a/scripts/storage-emulator-integration/import/flattened-emulator-data-missing-blobs-and-metadata/firebase-export-metadata.json b/scripts/storage-emulator-integration/import/flattened-emulator-data-missing-blobs-and-metadata/firebase-export-metadata.json new file mode 100644 index 00000000000..6568db544e7 --- /dev/null +++ b/scripts/storage-emulator-integration/import/flattened-emulator-data-missing-blobs-and-metadata/firebase-export-metadata.json @@ -0,0 +1,7 @@ +{ + "version": "10.4.2", + "storage": { + "version": "10.4.2", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/flattened-emulator-data-missing-blobs-and-metadata/storage_export/buckets.json b/scripts/storage-emulator-integration/import/flattened-emulator-data-missing-blobs-and-metadata/storage_export/buckets.json new file mode 100644 index 00000000000..5cdf3eb336f --- /dev/null +++ b/scripts/storage-emulator-integration/import/flattened-emulator-data-missing-blobs-and-metadata/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "fake-project-id.appspot.com" + } + ] +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/flattened-emulator-data/firebase-export-metadata.json b/scripts/storage-emulator-integration/import/flattened-emulator-data/firebase-export-metadata.json new file mode 100644 index 00000000000..6568db544e7 --- /dev/null +++ b/scripts/storage-emulator-integration/import/flattened-emulator-data/firebase-export-metadata.json @@ -0,0 +1,7 @@ +{ + "version": "10.4.2", + "storage": { + "version": "10.4.2", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/src/test/appdistro/mockdata/mock.apk b/scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/blobs/fake-project-id.appspot.com%2Ftest_upload.jpg similarity index 100% rename from src/test/appdistro/mockdata/mock.apk rename to scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/blobs/fake-project-id.appspot.com%2Ftest_upload.jpg diff --git a/scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/buckets.json b/scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/buckets.json new file mode 100644 index 00000000000..5cdf3eb336f --- /dev/null +++ b/scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "fake-project-id.appspot.com" + } + ] +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/metadata/fake-project-id.appspot.com%2Ftest_upload.jpg.json b/scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/metadata/fake-project-id.appspot.com%2Ftest_upload.jpg.json new file mode 100644 index 00000000000..bc5caae9e79 --- /dev/null +++ b/scripts/storage-emulator-integration/import/flattened-emulator-data/storage_export/metadata/fake-project-id.appspot.com%2Ftest_upload.jpg.json @@ -0,0 +1,20 @@ +{ + "name": "test_upload.jpg", + "bucket": "fake-project-id.appspot.com", + "contentType": "application/octet-stream", + "metageneration": 1, + "generation": 1648084940926, + "storageClass": "STANDARD", + "contentDisposition": "inline", + "cacheControl": "public, max-age=3600", + "contentEncoding": "identity", + "downloadTokens": [ + "c3c71086-95a8-445d-96e7-f625972de4b0" + ], + "etag": "PQJQBXRweACX9yRsBEInQjOJ/0s", + "timeCreated": "2022-03-24T01:22:20.926Z", + "updated": "2022-03-24T01:22:20.926Z", + "size": 0, + "md5Hash": "1B2M2Y8AsgTpgAmY7PhCfg==", + "crc32c": "0" +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/mapped-emulator-data/firebase-export-metadata.json b/scripts/storage-emulator-integration/import/mapped-emulator-data/firebase-export-metadata.json new file mode 100644 index 00000000000..478df83e4a5 --- /dev/null +++ b/scripts/storage-emulator-integration/import/mapped-emulator-data/firebase-export-metadata.json @@ -0,0 +1,7 @@ +{ + "version": "11.3.0", + "storage": { + "version": "11.3.0", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/blobs/eaa2803e-4374-44bd-98cb-cdd7d31e3347 b/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/blobs/eaa2803e-4374-44bd-98cb-cdd7d31e3347 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/buckets.json b/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/buckets.json new file mode 100644 index 00000000000..5cdf3eb336f --- /dev/null +++ b/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "fake-project-id.appspot.com" + } + ] +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/metadata/eaa2803e-4374-44bd-98cb-cdd7d31e3347.json b/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/metadata/eaa2803e-4374-44bd-98cb-cdd7d31e3347.json new file mode 100644 index 00000000000..bc5caae9e79 --- /dev/null +++ b/scripts/storage-emulator-integration/import/mapped-emulator-data/storage_export/metadata/eaa2803e-4374-44bd-98cb-cdd7d31e3347.json @@ -0,0 +1,20 @@ +{ + "name": "test_upload.jpg", + "bucket": "fake-project-id.appspot.com", + "contentType": "application/octet-stream", + "metageneration": 1, + "generation": 1648084940926, + "storageClass": "STANDARD", + "contentDisposition": "inline", + "cacheControl": "public, max-age=3600", + "contentEncoding": "identity", + "downloadTokens": [ + "c3c71086-95a8-445d-96e7-f625972de4b0" + ], + "etag": "PQJQBXRweACX9yRsBEInQjOJ/0s", + "timeCreated": "2022-03-24T01:22:20.926Z", + "updated": "2022-03-24T01:22:20.926Z", + "size": 0, + "md5Hash": "1B2M2Y8AsgTpgAmY7PhCfg==", + "crc32c": "0" +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/nested-emulator-data/firebase-export-metadata.json b/scripts/storage-emulator-integration/import/nested-emulator-data/firebase-export-metadata.json new file mode 100644 index 00000000000..6568db544e7 --- /dev/null +++ b/scripts/storage-emulator-integration/import/nested-emulator-data/firebase-export-metadata.json @@ -0,0 +1,7 @@ +{ + "version": "10.4.2", + "storage": { + "version": "10.4.2", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/blobs/fake-project-id.appspot.com/test_upload.jpg b/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/blobs/fake-project-id.appspot.com/test_upload.jpg new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/buckets.json b/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/buckets.json new file mode 100644 index 00000000000..5cdf3eb336f --- /dev/null +++ b/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "fake-project-id.appspot.com" + } + ] +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/metadata/fake-project-id.appspot.com/test_upload.jpg.json b/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/metadata/fake-project-id.appspot.com/test_upload.jpg.json new file mode 100644 index 00000000000..bc5caae9e79 --- /dev/null +++ b/scripts/storage-emulator-integration/import/nested-emulator-data/storage_export/metadata/fake-project-id.appspot.com/test_upload.jpg.json @@ -0,0 +1,20 @@ +{ + "name": "test_upload.jpg", + "bucket": "fake-project-id.appspot.com", + "contentType": "application/octet-stream", + "metageneration": 1, + "generation": 1648084940926, + "storageClass": "STANDARD", + "contentDisposition": "inline", + "cacheControl": "public, max-age=3600", + "contentEncoding": "identity", + "downloadTokens": [ + "c3c71086-95a8-445d-96e7-f625972de4b0" + ], + "etag": "PQJQBXRweACX9yRsBEInQjOJ/0s", + "timeCreated": "2022-03-24T01:22:20.926Z", + "updated": "2022-03-24T01:22:20.926Z", + "size": 0, + "md5Hash": "1B2M2Y8AsgTpgAmY7PhCfg==", + "crc32c": "0" +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/tests.ts b/scripts/storage-emulator-integration/import/tests.ts new file mode 100644 index 00000000000..118abe2f03e --- /dev/null +++ b/scripts/storage-emulator-integration/import/tests.ts @@ -0,0 +1,153 @@ +import * as path from "path"; +import * as fs from "fs-extra"; +import { expect } from "chai"; +import supertest = require("supertest"); + +import { createTmpDir } from "../../../src/emulator/testing/fixtures"; +import { Emulators } from "../../../src/emulator/types"; +import { TriggerEndToEndTest } from "../../integration-helpers/framework"; +import { + EMULATORS_SHUTDOWN_DELAY_MS, + FIREBASE_EMULATOR_CONFIG, + getStorageEmulatorHost, + readEmulatorConfig, + TEST_SETUP_TIMEOUT, +} from "../utils"; + +describe("Import Emulator Data", () => { + const FIREBASE_PROJECT = "fake-project-id"; + const BUCKET = `${FIREBASE_PROJECT}.appspot.com`; + const EMULATOR_CONFIG = readEmulatorConfig(FIREBASE_EMULATOR_CONFIG); + const STORAGE_EMULATOR_HOST = getStorageEmulatorHost(EMULATOR_CONFIG); + const test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, EMULATOR_CONFIG); + + it("retrieves file from imported flattened emulator data", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + await test.startEmulators([ + "--only", + Emulators.STORAGE, + "--import", + path.join(__dirname, "flattened-emulator-data"), + ]); + + await supertest(STORAGE_EMULATOR_HOST) + .get(`/v0/b/${BUCKET}/o/test_upload.jpg`) + .set({ Authorization: "Bearer owner" }) + .expect(200); + }); + + it("imports directory that is missing blobs and metadata directories", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + await test.startEmulators([ + "--only", + Emulators.STORAGE, + "--import", + path.join(__dirname, "flattened-emulator-data-missing-blobs-and-metadata"), + ]); + }); + + it("stores only the files as blobs when importing emulator data", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const exportedData = createTmpDir("exported-emulator-data"); + + // Import data and export it again on exit + await test.startEmulators([ + "--only", + Emulators.STORAGE, + "--import", + path.join(__dirname, "nested-emulator-data"), + "--export-on-exit", + exportedData, + ]); + await test.stopEmulators(); + + expect(fs.readdirSync(path.join(exportedData, "storage_export", "blobs")).length).to.equal(1); + }); + + it("retrieves file from imported nested emulator data", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + await test.startEmulators([ + "--only", + Emulators.STORAGE, + "--import", + path.join(__dirname, "nested-emulator-data"), + ]); + + await supertest(STORAGE_EMULATOR_HOST) + .get(`/v0/b/${BUCKET}/o/test_upload.jpg`) + .set({ Authorization: "Bearer owner" }) + .expect(200); + }); + + it("retrieves file from importing previously exported emulator data", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const exportedData = createTmpDir("exported-emulator-data"); + + // Upload file to Storage and export emulator data to tmp directory + await test.startEmulators(["--only", Emulators.STORAGE, "--export-on-exit", exportedData]); + const uploadURL = await supertest(STORAGE_EMULATOR_HOST) + .post(`/v0/b/${BUCKET}/o/test_upload.jpg?uploadType=resumable&name=test_upload.jpg`) + .set({ + Authorization: "Bearer owner", + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "start", + }) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(STORAGE_EMULATOR_HOST) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + }); + + await test.stopEmulators(); + + // Import previously exported emulator data and retrieve file from Storage + await test.startEmulators(["--only", Emulators.STORAGE, "--import", exportedData]); + await supertest(STORAGE_EMULATOR_HOST) + .get(`/v0/b/${BUCKET}/o/test_upload.jpg`) + .set({ Authorization: "Bearer owner" }) + .expect(200); + }); + + it("retrieves file from importing emulator data previously exported on Windows", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + await test.startEmulators([ + "--only", + Emulators.STORAGE, + "--import", + path.join(__dirname, "windows-emulator-data"), + ]); + + await supertest(STORAGE_EMULATOR_HOST) + .get(`/v0/b/${BUCKET}/o/test_upload.jpg`) + .set({ Authorization: "Bearer owner" }) + .expect(200); + + await supertest(STORAGE_EMULATOR_HOST) + .get(`/v0/b/${BUCKET}/o/test-directory%2Ftest_nested_upload.jpg`) + .set({ Authorization: "Bearer owner" }) + .expect(200); + }); + + afterEach(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + await test.stopEmulators(); + }); + + it("retrieves file from imported mapped emulator data", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + await test.startEmulators([ + "--only", + Emulators.STORAGE, + "--import", + path.join(__dirname, "mapped-emulator-data"), + ]); + + await supertest(STORAGE_EMULATOR_HOST) + .get(`/v0/b/${BUCKET}/o/test_upload.jpg`) + .set({ Authorization: "Bearer owner" }) + .expect(200); + }); +}); diff --git a/scripts/storage-emulator-integration/import/windows-emulator-data/firebase-export-metadata.json b/scripts/storage-emulator-integration/import/windows-emulator-data/firebase-export-metadata.json new file mode 100644 index 00000000000..6568db544e7 --- /dev/null +++ b/scripts/storage-emulator-integration/import/windows-emulator-data/firebase-export-metadata.json @@ -0,0 +1,7 @@ +{ + "version": "10.4.2", + "storage": { + "version": "10.4.2", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/blobs/fake-project-id.appspot.com%5Ctest-directory%5Ctest_nested_upload.jpg b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/blobs/fake-project-id.appspot.com%5Ctest-directory%5Ctest_nested_upload.jpg new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/blobs/fake-project-id.appspot.com%5Ctest_upload.jpg b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/blobs/fake-project-id.appspot.com%5Ctest_upload.jpg new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/buckets.json b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/buckets.json new file mode 100644 index 00000000000..5cdf3eb336f --- /dev/null +++ b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "fake-project-id.appspot.com" + } + ] +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/metadata/fake-project-id.appspot.com%5Ctest-directory%5Ctest_nested_upload.jpg.json b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/metadata/fake-project-id.appspot.com%5Ctest-directory%5Ctest_nested_upload.jpg.json new file mode 100644 index 00000000000..3afcc462964 --- /dev/null +++ b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/metadata/fake-project-id.appspot.com%5Ctest-directory%5Ctest_nested_upload.jpg.json @@ -0,0 +1,20 @@ +{ + "name": "test-directory\\test_nested_upload.jpg", + "bucket": "fake-project-id.appspot.com", + "contentType": "application/octet-stream", + "metageneration": 1, + "generation": 1648084940926, + "storageClass": "STANDARD", + "contentDisposition": "inline", + "cacheControl": "public, max-age=3600", + "contentEncoding": "identity", + "downloadTokens": [ + "c3c71086-95a8-445d-96e7-f625972de4b0" + ], + "etag": "PQJQBXRweACX9yRsBEInQjOJ/0s", + "timeCreated": "2022-03-24T01:22:20.926Z", + "updated": "2022-03-24T01:22:20.926Z", + "size": 0, + "md5Hash": "1B2M2Y8AsgTpgAmY7PhCfg==", + "crc32c": "0" +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/metadata/fake-project-id.appspot.com%5Ctest_upload.jpg.json b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/metadata/fake-project-id.appspot.com%5Ctest_upload.jpg.json new file mode 100644 index 00000000000..bc5caae9e79 --- /dev/null +++ b/scripts/storage-emulator-integration/import/windows-emulator-data/storage_export/metadata/fake-project-id.appspot.com%5Ctest_upload.jpg.json @@ -0,0 +1,20 @@ +{ + "name": "test_upload.jpg", + "bucket": "fake-project-id.appspot.com", + "contentType": "application/octet-stream", + "metageneration": 1, + "generation": 1648084940926, + "storageClass": "STANDARD", + "contentDisposition": "inline", + "cacheControl": "public, max-age=3600", + "contentEncoding": "identity", + "downloadTokens": [ + "c3c71086-95a8-445d-96e7-f625972de4b0" + ], + "etag": "PQJQBXRweACX9yRsBEInQjOJ/0s", + "timeCreated": "2022-03-24T01:22:20.926Z", + "updated": "2022-03-24T01:22:20.926Z", + "size": 0, + "md5Hash": "1B2M2Y8AsgTpgAmY7PhCfg==", + "crc32c": "0" +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/internal/tests.ts b/scripts/storage-emulator-integration/internal/tests.ts new file mode 100644 index 00000000000..aab89385a18 --- /dev/null +++ b/scripts/storage-emulator-integration/internal/tests.ts @@ -0,0 +1,168 @@ +import { expect } from "chai"; +import * as supertest from "supertest"; +import { StorageRulesFiles } from "../../../src/emulator/testing/fixtures"; +import { TriggerEndToEndTest } from "../../integration-helpers/framework"; +import { + EMULATORS_SHUTDOWN_DELAY_MS, + getStorageEmulatorHost, + readEmulatorConfig, + TEST_SETUP_TIMEOUT, +} from "../utils"; + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || "fake-project-id"; +const EMULATOR_CONFIG = readEmulatorConfig(); +const STORAGE_EMULATOR_HOST = getStorageEmulatorHost(EMULATOR_CONFIG); + +describe("Storage emulator internal endpoints", () => { + let test: TriggerEndToEndTest; + + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + process.env.STORAGE_EMULATOR_HOST = STORAGE_EMULATOR_HOST; + test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, EMULATOR_CONFIG); + await test.startEmulators(["--only", "auth,storage"]); + }); + + beforeEach(async () => { + // Reset emulator to default rules. + await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [StorageRulesFiles.readWriteIfAuth], + }, + }) + .expect(200); + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + delete process.env.STORAGE_EMULATOR_HOST; + await test.stopEmulators(); + }); + + describe("setRules", () => { + it("should set single ruleset", async () => { + await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [StorageRulesFiles.readWriteIfTrue], + }, + }) + .expect(200); + }); + + it("should set multiple rules/resource objects", async () => { + await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [ + { resource: "bucket_0", ...StorageRulesFiles.readWriteIfTrue }, + { resource: "bucket_1", ...StorageRulesFiles.readWriteIfAuth }, + ], + }, + }) + .expect(200); + }); + + it("should overwrite single ruleset with multiple rules/resource objects", async () => { + await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [StorageRulesFiles.readWriteIfTrue], + }, + }) + .expect(200); + + await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [ + { resource: "bucket_0", ...StorageRulesFiles.readWriteIfTrue }, + { resource: "bucket_1", ...StorageRulesFiles.readWriteIfAuth }, + ], + }, + }) + .expect(200); + }); + + it("should return 400 if rules.files array is missing", async () => { + const errorMessage = await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ rules: {} }) + .expect(400) + .then((res) => res.body.message); + + expect(errorMessage).to.equal("Request body must include 'rules.files' array"); + }); + + it("should return 400 if rules.files array has missing name field", async () => { + const errorMessage = await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [{ content: StorageRulesFiles.readWriteIfTrue.content }], + }, + }) + .expect(400) + .then((res) => res.body.message); + + expect(errorMessage).to.equal( + "Each member of 'rules.files' array must contain 'name' and 'content'", + ); + }); + + it("should return 400 if rules.files array has missing content field", async () => { + const errorMessage = await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [{ name: StorageRulesFiles.readWriteIfTrue.name }], + }, + }) + .expect(400) + .then((res) => res.body.message); + + expect(errorMessage).to.equal( + "Each member of 'rules.files' array must contain 'name' and 'content'", + ); + }); + + it("should return 400 if rules.files array has missing resource field", async () => { + const errorMessage = await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [ + { resource: "bucket_0", ...StorageRulesFiles.readWriteIfTrue }, + StorageRulesFiles.readWriteIfAuth, + ], + }, + }) + .expect(400) + .then((res) => res.body.message); + + expect(errorMessage).to.equal( + "Each member of 'rules.files' array must contain 'name', 'content', and 'resource'", + ); + }); + + it("should return 400 if rules.files array has invalid content", async () => { + const errorMessage = await supertest(STORAGE_EMULATOR_HOST) + .put("/internal/setRules") + .send({ + rules: { + files: [{ name: StorageRulesFiles.readWriteIfTrue.name, content: "foo" }], + }, + }) + .expect(400) + .then((res) => res.body.message); + + expect(errorMessage).to.equal("There was an error updating rules, see logs for more details"); + }); + }); +}); diff --git a/scripts/storage-emulator-integration/multiple-targets/.firebaserc b/scripts/storage-emulator-integration/multiple-targets/.firebaserc new file mode 100644 index 00000000000..261bc3f13d7 --- /dev/null +++ b/scripts/storage-emulator-integration/multiple-targets/.firebaserc @@ -0,0 +1,17 @@ +{ + "projects": {}, + "targets": { + "fake-project-id": { + "storage": { + "allowNone": [ + "fake-project-id.appspot.com" + ], + "allowAll": [ + "fake-project-id-2.appspot.com" + ] + } + } + }, + "etags": {}, + "dataconnectEmulatorConfig": {} +} \ No newline at end of file diff --git a/scripts/storage-emulator-integration/multiple-targets/allowAll.rules b/scripts/storage-emulator-integration/multiple-targets/allowAll.rules new file mode 100644 index 00000000000..a7db6961cad --- /dev/null +++ b/scripts/storage-emulator-integration/multiple-targets/allowAll.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if true; + } + } +} diff --git a/scripts/storage-emulator-integration/multiple-targets/allowNone.rules b/scripts/storage-emulator-integration/multiple-targets/allowNone.rules new file mode 100644 index 00000000000..9f33d22cbd7 --- /dev/null +++ b/scripts/storage-emulator-integration/multiple-targets/allowNone.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } +} diff --git a/scripts/storage-emulator-integration/multiple-targets/firebase.json b/scripts/storage-emulator-integration/multiple-targets/firebase.json new file mode 100644 index 00000000000..3448de54697 --- /dev/null +++ b/scripts/storage-emulator-integration/multiple-targets/firebase.json @@ -0,0 +1,19 @@ +{ + "storage": [ + { + "target": "allowNone", + "rules": "allowNone.rules" + }, + { + "target": "allowAll", + "rules": "allowAll.rules" + } + ], + "emulators": { + "storage": { + "port": 9199 + } + } + } + + \ No newline at end of file diff --git a/scripts/storage-emulator-integration/multiple-targets/tests.ts b/scripts/storage-emulator-integration/multiple-targets/tests.ts new file mode 100644 index 00000000000..a010d923884 --- /dev/null +++ b/scripts/storage-emulator-integration/multiple-targets/tests.ts @@ -0,0 +1,64 @@ +import supertest = require("supertest"); +import { Emulators } from "../../../src/emulator/types"; +import { TriggerEndToEndTest } from "../../integration-helpers/framework"; +import { + EMULATORS_SHUTDOWN_DELAY_MS, + FIREBASE_EMULATOR_CONFIG, + getStorageEmulatorHost, + readEmulatorConfig, + TEST_SETUP_TIMEOUT, +} from "../utils"; + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || "fake-project-id"; + +describe("Multiple Storage Deploy Targets", () => { + let test: TriggerEndToEndTest; + const allowNoneBucket = `${FIREBASE_PROJECT}.appspot.com`; + const allowAllBucket = `${FIREBASE_PROJECT}-2.appspot.com`; + const emulatorConfig = readEmulatorConfig(FIREBASE_EMULATOR_CONFIG); + const STORAGE_EMULATOR_HOST = getStorageEmulatorHost(emulatorConfig); + + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, emulatorConfig); + await test.applyTargets(Emulators.STORAGE, "allowNone", allowNoneBucket); + await test.applyTargets(Emulators.STORAGE, "allowAll", allowAllBucket); + await test.startEmulators(["--only", Emulators.STORAGE]); + }); + + it("should enforce different rules for different targets", async () => { + const uploadURL = await supertest(STORAGE_EMULATOR_HOST) + .post(`/v0/b/${allowNoneBucket}/o/test_upload.jpg?uploadType=resumable&name=test_upload.jpg`) + .set({ "X-Goog-Upload-Protocol": "resumable", "X-Goog-Upload-Command": "start" }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(STORAGE_EMULATOR_HOST) + .put(uploadURL.pathname + uploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + }) + .expect(403); + + const otherUploadURL = await supertest(STORAGE_EMULATOR_HOST) + .post(`/v0/b/${allowAllBucket}/o/test_upload.jpg?uploadType=resumable&name=test_upload.jpg`) + .set({ "X-Goog-Upload-Protocol": "resumable", "X-Goog-Upload-Command": "start" }) + .expect(200) + .then((res) => new URL(res.header["x-goog-upload-url"])); + + await supertest(STORAGE_EMULATOR_HOST) + .put(otherUploadURL.pathname + otherUploadURL.search) + .set({ + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Command": "upload, finalize", + }) + .expect(200); + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + await test.stopEmulators(); + }); +}); diff --git a/scripts/storage-emulator-integration/rules/manager.test.ts b/scripts/storage-emulator-integration/rules/manager.test.ts new file mode 100644 index 00000000000..7d594a8b101 --- /dev/null +++ b/scripts/storage-emulator-integration/rules/manager.test.ts @@ -0,0 +1,129 @@ +import { expect } from "chai"; + +import { createTmpDir, StorageRulesFiles } from "../../../src/emulator/testing/fixtures"; +import { createStorageRulesManager } from "../../../src/emulator/storage/rules/manager"; +import { StorageRulesRuntime } from "../../../src/emulator/storage/rules/runtime"; +import * as fs from "fs"; +import { RulesetOperationMethod, SourceFile } from "../../../src/emulator/storage/rules/types"; +import { isPermitted } from "../../../src/emulator/storage/rules/utils"; +import { readFile } from "../../../src/fsutils"; +import * as path from "path"; + +const EMULATOR_LOAD_RULESET_DELAY_MS = 20000; +const SETUP_TIMEOUT = 60000; +const PROJECT_ID = "demo-project-id"; + +describe("Storage Rules Manager", () => { + let rulesRuntime: StorageRulesRuntime; + const opts = { method: RulesetOperationMethod.GET, file: {}, path: "/b/bucket_0/o/" }; + + before(async function (this) { + this.timeout(SETUP_TIMEOUT); + rulesRuntime = new StorageRulesRuntime(); + await rulesRuntime.start(); + }); + + after(async function (this) { + this.timeout(SETUP_TIMEOUT); + await rulesRuntime.stop(); + }); + + it("should load multiple rulesets on start", async function (this) { + this.timeout(SETUP_TIMEOUT); + const rules = [ + { resource: "bucket_0", rules: StorageRulesFiles.readWriteIfTrue }, + { resource: "bucket_1", rules: StorageRulesFiles.readWriteIfAuth }, + ]; + const rulesManager = createStorageRulesManager(rules, rulesRuntime); + await rulesManager.start(); + + const bucket0Ruleset = rulesManager.getRuleset("bucket_0"); + expect( + await isPermitted({ + ...opts, + path: "/b/bucket_0/o/", + ruleset: bucket0Ruleset!, + projectId: PROJECT_ID, + }), + ).to.be.true; + + const bucket1Ruleset = rulesManager.getRuleset("bucket_1"); + expect( + await isPermitted({ + ...opts, + path: "/b/bucket_1/o/", + ruleset: bucket1Ruleset!, + projectId: PROJECT_ID, + }), + ).to.be.false; + + await rulesManager.stop(); + }); + + it("should load single ruleset on start", async function (this) { + this.timeout(SETUP_TIMEOUT); + // Write rules to file + const fileName = "storage.rules"; + const testDir = createTmpDir("storage-files"); + appendBytes(testDir, fileName, Buffer.from(StorageRulesFiles.readWriteIfTrue.content)); + + const sourceFile = getSourceFile(testDir, fileName); + const rulesManager = createStorageRulesManager(sourceFile, rulesRuntime); + await rulesManager.start(); + + const ruleset = rulesManager.getRuleset("bucket"); + expect(await isPermitted({ ...opts, ruleset: ruleset!, projectId: PROJECT_ID })).to.be.true; + + await rulesManager.stop(); + }); + + it("should reload ruleset on changes to source file", async function (this) { + this.timeout(SETUP_TIMEOUT); + // Write rules to file + const fileName = "storage.rules"; + const testDir = createTmpDir("storage-files"); + appendBytes(testDir, fileName, Buffer.from(StorageRulesFiles.readWriteIfTrue.content)); + + const sourceFile = getSourceFile(testDir, fileName); + const rulesManager = createStorageRulesManager(sourceFile, rulesRuntime); + await rulesManager.start(); + + expect( + await isPermitted({ + ...opts, + ruleset: rulesManager.getRuleset("bucket")!, + projectId: PROJECT_ID, + }), + ).to.be.true; + + // Write new rules to file + deleteFile(testDir, fileName); + appendBytes(testDir, fileName, Buffer.from(StorageRulesFiles.readWriteIfAuth.content)); + + await new Promise((resolve) => setTimeout(resolve, EMULATOR_LOAD_RULESET_DELAY_MS)); + expect( + await isPermitted({ + ...opts, + ruleset: rulesManager.getRuleset("bucket")!, + projectId: PROJECT_ID, + }), + ).to.be.false; + + await rulesManager.stop(); + }); +}); + +function getSourceFile(testDir: string, fileName: string): SourceFile { + const filePath = `${testDir}/${fileName}`; + return { name: filePath, content: readFile(filePath) }; +} + +function appendBytes(dirPath: string, fileName: string, bytes: Buffer): void { + const filepath = path.join(dirPath, encodeURIComponent(fileName)); + fs.appendFileSync(filepath, bytes); +} + +function deleteFile(dirPath: string, fileName: string): void { + const filepath = path.join(dirPath, encodeURIComponent(fileName)); + fs.unlinkSync(filepath); +} diff --git a/scripts/storage-emulator-integration/rules/runtime.test.ts b/scripts/storage-emulator-integration/rules/runtime.test.ts new file mode 100644 index 00000000000..f82d5e1bba3 --- /dev/null +++ b/scripts/storage-emulator-integration/rules/runtime.test.ts @@ -0,0 +1,411 @@ +import { + RulesetVerificationOpts, + StorageRulesRuntime, +} from "../../../src/emulator/storage/rules/runtime"; +import { expect } from "chai"; +import { StorageRulesFiles } from "../../emulator-tests/fixtures"; +import * as jwt from "jsonwebtoken"; +import { EmulatorLogger } from "../../../src/emulator/emulatorLogger"; +import { ExpressionValue } from "../../../src/emulator/storage/rules/expressionValue"; +import { RulesetOperationMethod } from "../../../src/emulator/storage/rules/types"; +import { downloadIfNecessary } from "../../../src/emulator/downloadableEmulators"; +import { Emulators } from "../../../src/emulator/types"; +import { RulesResourceMetadata } from "../../../src/emulator/storage/metadata"; + +const TOKENS = { + signedInUser: jwt.sign( + { + user_id: "mock-user", + }, + "mock-secret", + ), +}; + +function createFakeResourceMetadata(params: { + size?: number; + md5Hash?: string; +}): RulesResourceMetadata { + return { + name: "files/goat", + bucket: "fake-app.appspot.com", + generation: 1, + metageneration: 1, + size: params.size ?? 1024 /* 1 KiB */, + timeCreated: new Date(), + updated: new Date(), + md5Hash: params.md5Hash ?? "fake-md5-hash", + crc32c: "fake-crc32c", + etag: "fake-etag", + contentDisposition: "", + contentEncoding: "", + contentType: "", + metadata: {}, + }; +} + +describe("Storage Rules Runtime", function () { + let runtime: StorageRulesRuntime; + + // eslint-disable-next-line @typescript-eslint/no-invalid-this + this.timeout(10000); + + before(async () => { + await downloadIfNecessary(Emulators.STORAGE); + + runtime = new StorageRulesRuntime(); + (EmulatorLogger as any).prototype.log = console.log.bind(console); + await runtime.start(); + }); + + after(() => { + runtime.stop(); + }); + + it("should have a living child process", () => { + expect(runtime.alive).to.be.true; + }); + + it("should load a basic ruleset", async () => { + const { ruleset } = await runtime.loadRuleset({ + files: [StorageRulesFiles.readWriteIfAuth], + }); + + expect(ruleset).to.not.be.undefined; + }); + + it("should send errors on invalid ruleset compilation", async () => { + const { ruleset, issues } = await runtime.loadRuleset({ + files: [ + { + name: "/dev/null/storage.rules", + content: ` + rules_version = '2'; + // Extra brace in the following line + service firebase.storage {{ + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if request.auth!=null; + } + } + } + `, + }, + ], + }); + + expect(ruleset).to.be.undefined; + expect(issues.errors.length).to.gt(0); + }); + + it("should reject an invalid evaluation", async () => { + expect( + await testIfPermitted( + runtime, + ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } + } + `, + { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/num_check/filename.jpg", + file: {}, + }, + ), + ).to.be.false; + }); + + it("should accept a value evaluation", async () => { + expect( + await testIfPermitted( + runtime, + ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if true; + } + } + } + `, + { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/num_check/filename.jpg", + file: {}, + }, + ), + ).to.be.true; + }); + + describe("request", () => { + describe(".auth", () => { + it("can read from auth.uid", async () => { + expect( + await testIfPermitted( + runtime, + ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o/{sizeSegment=**} { + allow read: if request.auth.uid == 'mock-user'; + } + } + `, + { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/sizes/md", + file: {}, + }, + ), + ).to.be.true; + }); + + it("allows only authenticated reads", async () => { + const rulesContent = ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o/{sizeSegment=**} { + allow read: if request.auth != null; + } + } + `; + + // Authenticated reads are allowed + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/sizes/md", + file: {}, + }), + ).to.be.true; + // Authenticated writes are not allowed + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.WRITE, + path: "/b/BUCKET_NAME/o/sizes/md", + file: {}, + }), + ).to.be.false; + // Unautheticated reads are not allowed + expect( + await testIfPermitted(runtime, rulesContent, { + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/sizes/md", + file: {}, + }), + ).to.be.false; + // Unautheticated writes are not allowed + expect( + await testIfPermitted(runtime, rulesContent, { + method: RulesetOperationMethod.WRITE, + path: "/b/BUCKET_NAME/o/sizes/md", + file: {}, + }), + ).to.be.false; + }); + }); + + it(".path rules are respected", async () => { + const rulesContent = ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + // Test that request.path is relative to the service (firebase.storage) + allow read: if request.path[0] == "b" && + request.path[1] == "BUCKET_NAME" && + request.path[2] == "o" && + request.path[3] == "dir" && + request.path[4] == "subdir" && + request.path[5] == "image.png" && + request.path == path("/b/BUCKET_NAME/o/dir/subdir/image.png"); + } + } + }`; + + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/dir/subdir/disallowed.png", + file: {}, + }), + ).to.be.false; + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/dir/subdir/image.png", + file: {}, + }), + ).to.be.true; + }); + + it(".resource rules are respected", async () => { + const rulesContent = ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /files/{file} { + allow read, write: if request.resource.size < 5 * 1024 * 1024; + } + } + }`; + + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.WRITE, + path: "/b/BUCKET_NAME/o/files/goat", + file: { after: createFakeResourceMetadata({ size: 500 * 1024 /* 500 KiB */ }) }, + }), + ).to.be.true; + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.WRITE, + path: "/b/BUCKET_NAME/o/files/goat", + file: { after: createFakeResourceMetadata({ size: 10 * 1024 * 1024 /* 10 MiB */ }) }, + }), + ).to.be.false; + }); + }); + + describe("resource", () => { + it("should only read for small files", async () => { + const rulesContent = ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /files/{file} { + allow read: if resource.size < 5 * 1024 * 1024; + allow write: if false; + } + } + }`; + + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/files/goat", + file: { before: createFakeResourceMetadata({ size: 500 * 1024 /* 500 KiB */ }) }, + }), + ).to.be.true; + + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/files/goat", + file: { before: createFakeResourceMetadata({ size: 10 * 1024 * 1024 /* 10 MiB */ }) }, + }), + ).to.be.false; + }); + + it("should only permit upload if hash matches", async () => { + const rulesContent = ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /files/{file} { + allow read, write: if request.resource.md5Hash == resource.md5Hash; + } + } + }`; + const metadata1 = createFakeResourceMetadata({ md5Hash: "fake-md5-hash" }); + const metadata2 = createFakeResourceMetadata({ md5Hash: "different-md5-hash" }); + + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/files/goat", + file: { before: metadata1, after: metadata1 }, + }), + ).to.be.true; + expect( + await testIfPermitted(runtime, rulesContent, { + token: TOKENS.signedInUser, + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/files/goat", + file: { before: metadata1, after: metadata2 }, + }), + ).to.be.false; + }); + }); + + describe("features", () => { + describe("ternary", () => { + it("should support ternary operators", async () => { + const rulesContent = ` + rules_version = '2'; + service firebase.storage { + match /b/{bucket}/o { + match /{file} { + allow read: if request.path[3] == "test" ? true : false; + } + } + }`; + + expect( + await testIfPermitted(runtime, rulesContent, { + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/test", + file: {}, + }), + ).to.be.true; + + expect( + await testIfPermitted(runtime, rulesContent, { + method: RulesetOperationMethod.GET, + path: "/b/BUCKET_NAME/o/someRandomFile", + file: {}, + }), + ).to.be.false; + }); + }); + }); +}); + +async function testIfPermitted( + runtime: StorageRulesRuntime, + rulesetContent: string, + verificationOpts: Omit, + runtimeVariableOverrides: { [s: string]: ExpressionValue } = {}, +) { + const loadResult = await runtime.loadRuleset({ + files: [ + { + name: "/dev/null/storage.rules", + content: rulesetContent, + }, + ], + }); + + if (!loadResult.ruleset) { + throw new Error(JSON.stringify(loadResult.issues, undefined, 2)); + } + + const { permitted, issues } = await loadResult.ruleset.verify( + { ...verificationOpts, projectId: "demo-project-id" }, + runtimeVariableOverrides, + ); + + if (permitted === undefined) { + throw new Error(JSON.stringify(issues, undefined, 2)); + } + + return permitted; +} diff --git a/scripts/storage-emulator-integration/run.sh b/scripts/storage-emulator-integration/run.sh index e13154ce737..f2152963321 100755 --- a/scripts/storage-emulator-integration/run.sh +++ b/scripts/storage-emulator-integration/run.sh @@ -2,13 +2,20 @@ set -e # Immediately exit on failure # Globally link the CLI for the testing framework -./scripts/npm-link.sh +./scripts/clean-install.sh + +# Set application default credentials. +source scripts/set-default-credentials.sh # Prepare the storage emulator rules runtime firebase setup:emulators:storage -mocha \ - --require ts-node/register \ - --require source-map-support/register \ - --require src/test/helpers/mocha-bootstrap.ts \ - scripts/storage-emulator-integration/tests.ts +mocha scripts/storage-emulator-integration/internal/tests.ts + +mocha scripts/storage-emulator-integration/rules/*.test.ts + +mocha scripts/storage-emulator-integration/import/tests.ts + +mocha scripts/storage-emulator-integration/multiple-targets/tests.ts + +mocha scripts/storage-emulator-integration/conformance/*.test.ts diff --git a/scripts/storage-emulator-integration/storage.rules b/scripts/storage-emulator-integration/storage.rules index 79833da03d6..b1a51eb9d22 100644 --- a/scripts/storage-emulator-integration/storage.rules +++ b/scripts/storage-emulator-integration/storage.rules @@ -1,12 +1,43 @@ rules_version = '2'; service firebase.storage { match /b/{bucket}/o { - match /{allPaths=**} { - allow read, write: if request.auth != null; + match /topLevel { + allow list; + } + + match /disallowSize0 { + allow create: if request.resource.size != 0; + } + + match /testing/{allPaths=**} { + allow read, create, update, delete: if request.auth != null; + } + + match /listAll/{allPaths=**} { + allow list; + } + + match /delete { + match /disallowIfContentTypeText { + allow create; + allow delete: if resource.contentType != 'text/plain'; + } + } + + match /upload { + match /allowIfContentTypeImage.png { + allow create: if request.resource.contentType == 'image/blah'; + } + match /replace.txt { + allow read, create; + } + match /allowIfNoExistingFile.txt { + allow create: if resource == null; + } } match /public/{allPaths=**} { - allow read; + allow read, write; } } } diff --git a/scripts/storage-emulator-integration/tests.ts b/scripts/storage-emulator-integration/tests.ts deleted file mode 100644 index d1a6adaa5ab..00000000000 --- a/scripts/storage-emulator-integration/tests.ts +++ /dev/null @@ -1,1327 +0,0 @@ -import { expect } from "chai"; -import * as admin from "firebase-admin"; -import * as firebase from "firebase"; -import * as fs from "fs"; -import * as path from "path"; -import * as http from "http"; -import * as https from "https"; -import * as puppeteer from "puppeteer"; -import * as request from "request"; -import * as crypto from "crypto"; -import * as os from "os"; -import { Bucket, Storage } from "@google-cloud/storage"; -import supertest = require("supertest"); - -import { IMAGE_FILE_BASE64 } from "../../src/test/emulators/fixtures"; -import { FrameworkOptions, TriggerEndToEndTest } from "../integration-helpers/framework"; - -const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || "fake-project-id"; - -/* - * Various delays that are needed because this test spawns - * parallel emulator subprocesses. - */ -const TEST_SETUP_TIMEOUT = 60000; -const EMULATORS_SHUTDOWN_DELAY_MS = 5000; - -// Flip these flags for options during test debugging -// all should be FALSE on commit -const TEST_CONFIG = { - // Set this to true to use production servers - // (useful for writing tests against source of truth) - useProductionServers: false, - - // Set this to true to log all emulator logs to console - // (useful for debugging) - useMockedLogging: false, - - // Set this to true to make the headless chrome window visible - // (useful for ensuring the browser is running as expected) - showBrowser: false, - - // Set this to true to keep the browser open after tests finish - // (useful for checking browser logs for errors) - keepBrowserOpen: false, -}; - -// Files contianing the Firebase App Config and Service Account key for -// the app to be used in these tests.This is only applicable if -// TEST_CONFIG.useProductionServers is true -const PROD_APP_CONFIG = "storage-integration-config.json"; -const SERVICE_ACCOUNT_KEY = "service-account-key.json"; - -// Firebase Emulator config, for starting up emulators -const FIREBASE_EMULATOR_CONFIG = "firebase.json"; -const SMALL_FILE_SIZE = 200 * 1024; /* 200 kB */ -const LARGE_FILE_SIZE = 20 * 1024 * 1024; /* 20 MiB */ -// Temp directory to store generated files. -let tmpDir: string; - -/** - * Reads a JSON file in the current directory. - * - * @param filename name of the JSON file to be read. Must be in the current directory. - */ -function readJson(filename: string) { - const fullPath = path.join(__dirname, filename); - if (!fs.existsSync(fullPath)) { - throw new Error(`Can't find file at ${filename}`); - } - const data = fs.readFileSync(fullPath, "utf8"); - return JSON.parse(data); -} - -function readProdAppConfig() { - try { - return readJson(PROD_APP_CONFIG); - } catch (error) { - throw new Error( - `Cannot read the integration config. Please ensure that the file ${PROD_APP_CONFIG} is present in the current directory.` - ); - } -} - -function readEmulatorConfig(): FrameworkOptions { - try { - return readJson(FIREBASE_EMULATOR_CONFIG); - } catch (error) { - throw new Error( - `Cannot read the emulator config. Please ensure that the file ${FIREBASE_EMULATOR_CONFIG} is present in the current directory.` - ); - } -} - -function getAuthEmulatorHost(emulatorConfig: FrameworkOptions) { - const port = emulatorConfig.emulators?.auth?.port; - if (port) { - return `http://localhost:${port}`; - } - throw new Error("Auth emulator config not found or invalid"); -} - -function getStorageEmulatorHost(emulatorConfig: FrameworkOptions) { - const port = emulatorConfig.emulators?.storage?.port; - if (port) { - return `http://localhost:${port}`; - } - throw new Error("Storage emulator config not found or invalid"); -} - -function createRandomFile(filename: string, sizeInBytes: number): string { - if (!tmpDir) { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "storage-files")); - } - const fullPath = path.join(tmpDir, filename); - const bytes = crypto.randomBytes(sizeInBytes); - fs.writeFileSync(fullPath, bytes); - - return fullPath; -} - -/** - * Resets the storage layer of the Storage Emulator. - */ -async function resetStorageEmulator(emulatorHost: string) { - await new Promise((resolve) => { - request.post(`${emulatorHost}/internal/reset`, () => { - resolve(); - }); - }); -} - -describe("Storage emulator", () => { - let test: TriggerEndToEndTest; - let browser: puppeteer.Browser; - let page: puppeteer.Page; - - let smallFilePath: string; - let largeFilePath: string; - - // Emulators accept fake app configs. This is sufficient for testing against the emulator. - const FAKE_APP_CONFIG = { - apiKey: "fake-api-key", - projectId: `${FIREBASE_PROJECT}`, - authDomain: `${FIREBASE_PROJECT}.firebaseapp.com`, - storageBucket: `${FIREBASE_PROJECT}.appspot.com`, - appId: "fake-app-id", - }; - - const appConfig = TEST_CONFIG.useProductionServers ? readProdAppConfig() : FAKE_APP_CONFIG; - const emulatorConfig = readEmulatorConfig(); - - const storageBucket = appConfig.storageBucket; - const STORAGE_EMULATOR_HOST = getStorageEmulatorHost(emulatorConfig); - const AUTH_EMULATOR_HOST = getAuthEmulatorHost(emulatorConfig); - - const emulatorSpecificDescribe = TEST_CONFIG.useProductionServers ? describe.skip : describe; - - describe("Admin SDK Endpoints", function (this) { - // eslint-disable-next-line @typescript-eslint/no-invalid-this - this.timeout(TEST_SETUP_TIMEOUT); - let testBucket: Bucket; - - before(async () => { - if (!TEST_CONFIG.useProductionServers) { - process.env.STORAGE_EMULATOR_HOST = STORAGE_EMULATOR_HOST; - - test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, emulatorConfig); - await test.startEmulators(["--only", "auth,storage"]); - } - - // TODO: We should not need a real credential for emulator tests, but - // today we do. - const credential = fs.existsSync(path.join(__dirname, SERVICE_ACCOUNT_KEY)) - ? admin.credential.cert(readJson(SERVICE_ACCOUNT_KEY)) - : admin.credential.applicationDefault(); - - admin.initializeApp({ - credential, - }); - - testBucket = admin.storage().bucket(storageBucket); - - smallFilePath = createRandomFile("small_file", SMALL_FILE_SIZE); - largeFilePath = createRandomFile("large_file", LARGE_FILE_SIZE); - }); - - beforeEach(async () => { - if (!TEST_CONFIG.useProductionServers) { - await resetStorageEmulator(STORAGE_EMULATOR_HOST); - } else { - await testBucket.deleteFiles(); - } - }); - - describe(".bucket()", () => { - describe("#upload()", () => { - it("should handle non-resumable uploads", async () => { - await testBucket.upload(smallFilePath, { - resumable: false, - }); - // Doesn't require an assertion, will throw on failure - }); - - it("should replace existing file on upload", async () => { - const path = "replace.txt"; - const content1 = createRandomFile("small_content_1", 10); - const content2 = createRandomFile("small_content_2", 10); - const file = testBucket.file(path); - - await testBucket.upload(content1, { - destination: path, - }); - - const [readContent1] = await file.download(); - - expect(readContent1).to.deep.equal(fs.readFileSync(content1)); - - await testBucket.upload(content2, { - destination: path, - }); - - const [readContent2] = await file.download(); - expect(readContent2).to.deep.equal(fs.readFileSync(content2)); - - fs.unlinkSync(content1); - fs.unlinkSync(content2); - }); - - it("should handle gzip'd uploads", async () => { - // This appears to pass, but the file gets corrupted cause it's gzipped? - // expect(true).to.be.false; - await testBucket.upload(smallFilePath, { - gzip: true, - }); - }); - - // TODO(abehaskins): This test is temporarily disabled due to a credentials issue - it.skip("should handle large (resumable) uploads", async () => { - await testBucket.upload(largeFilePath), - { - resumable: true, - }; - }); - }); - - describe("#getFiles()", () => { - it("should list files", async () => { - await testBucket.upload(smallFilePath, { - destination: "testing/shoveler.svg", - }); - const [files, prefixes] = await testBucket.getFiles({ - directory: "testing", - }); - - expect(prefixes).to.be.undefined; - expect(files.map((file) => file.name)).to.deep.equal(["testing/shoveler.svg"]); - }); - }); - }); - - describe(".file()", () => { - describe("#save()", () => { - // TODO(abehaskins): This test is temporarily disabled due to a credentials issue - it.skip("should accept a zero-byte file", async () => { - await testBucket.file("testing/dir/").save(""); - - const [files] = await testBucket.getFiles({ - directory: "testing", - }); - - expect(files.map((file) => file.name)).to.contain("testing/dir/"); - }); - }); - - describe("#get()", () => { - // TODO(abehaskins): This test is temporarily disabled due to a credentials issue - it.skip("should complete an save/get/download cycle", async () => { - const p = "testing/dir/hello.txt"; - const content = "hello, world"; - - await testBucket.file(p).save(content); - - const [f] = await testBucket.file(p).get(); - const [buf] = await f.download(); - - expect(buf.toString()).to.equal(content); - }); - }); - - describe("#delete()", () => { - it("should properly delete a file from the bucket", async () => { - // We use a nested path to ensure that we don't need to decode - // the objectId in the gcloud emulator API - const bucketFilePath = "file/to/delete"; - await testBucket.upload(smallFilePath, { - destination: bucketFilePath, - }); - - // Get a reference to the uploaded file - const toDeleteFile = testBucket.file(bucketFilePath); - - // Ensure that the file exists on the bucket before deleting it - const [existsBefore] = await toDeleteFile.exists(); - expect(existsBefore).to.equal(true); - - // Delete it - await toDeleteFile.delete(); - // Ensure that it doesn't exist anymore on the bucket - const [existsAfter] = await toDeleteFile.exists(); - expect(existsAfter).to.equal(false); - }); - }); - - describe("#download()", () => { - it("should return the content of the file", async () => { - await testBucket.upload(smallFilePath); - const [downloadContent] = await testBucket - .file(smallFilePath.split("/").slice(-1)[0]) - .download(); - - const actualContent = fs.readFileSync(smallFilePath); - expect(downloadContent).to.deep.equal(actualContent); - }); - }); - - describe("#makePublic()", () => { - it("should no-op", async () => { - const destination = "a/b"; - await testBucket.upload(smallFilePath, { destination }); - const [aclMetadata] = await testBucket.file(destination).makePublic(); - - const generation = aclMetadata.generation; - delete aclMetadata.generation; - - expect(aclMetadata).to.deep.equal({ - kind: "storage#objectAccessControl", - object: destination, - id: `${testBucket.name}/${destination}/${generation}/allUsers`, - selfLink: `${STORAGE_EMULATOR_HOST}/storage/v1/b/${ - testBucket.name - }/o/${encodeURIComponent(destination)}/acl/allUsers`, - bucket: testBucket.name, - entity: "allUsers", - role: "READER", - etag: "someEtag", - }); - }); - - it("should not interfere with downloading of bytes via public URL", async () => { - const destination = "a/b"; - await testBucket.upload(smallFilePath, { destination }); - await testBucket.file(destination).makePublic(); - - const publicLink = `${STORAGE_EMULATOR_HOST}/${testBucket.name}/${destination}`; - - const requestClient = TEST_CONFIG.useProductionServers ? https : http; - await new Promise((resolve, reject) => { - requestClient.get(publicLink, {}, (response) => { - const data: any = []; - response - .on("data", (chunk) => data.push(chunk)) - .on("end", () => { - expect(Buffer.concat(data).length).to.equal(SMALL_FILE_SIZE); - }) - .on("close", resolve) - .on("error", reject); - }); - }); - }); - }); - - describe("#getMetadata()", () => { - it("should throw on non-existing file", async () => { - let err: any; - await testBucket - .file(smallFilePath) - .getMetadata() - .catch((_err) => { - err = _err; - }); - - expect(err).to.not.be.empty; - }); - - it("should return generated metadata for new upload", async () => { - await testBucket.upload(smallFilePath); - const [metadata] = await testBucket - .file(smallFilePath.split("/").slice(-1)[0]) - .getMetadata(); - - const metadataTypes: { [s: string]: string } = {}; - - for (const key in metadata) { - if (metadata[key]) { - metadataTypes[key] = typeof metadata[key]; - } - } - - expect(metadataTypes).to.deep.equal({ - bucket: "string", - contentType: "string", - generation: "string", - md5Hash: "string", - crc32c: "string", - etag: "string", - metageneration: "string", - storageClass: "string", - name: "string", - size: "string", - timeCreated: "string", - updated: "string", - id: "string", - kind: "string", - mediaLink: "string", - selfLink: "string", - timeStorageClassUpdated: "string", - }); - }); - - it("should return a functional media link", async () => { - await testBucket.upload(smallFilePath); - const [{ mediaLink }] = await testBucket - .file(smallFilePath.split("/").slice(-1)[0]) - .getMetadata(); - - const requestClient = TEST_CONFIG.useProductionServers ? https : http; - await new Promise((resolve, reject) => { - requestClient.get(mediaLink, {}, (response) => { - const data: any = []; - response - .on("data", (chunk) => data.push(chunk)) - .on("end", () => { - expect(Buffer.concat(data).length).to.equal(SMALL_FILE_SIZE); - }) - .on("close", resolve) - .on("error", reject); - }); - }); - }); - - it("should handle firebaseStorageDownloadTokens", async () => { - const destination = "public/small_file"; - await testBucket.upload(smallFilePath, { - destination, - metadata: {}, - }); - - const cloudFile = testBucket.file(destination); - const md = { - metadata: { - firebaseStorageDownloadTokens: "myFirstToken,mySecondToken", - }, - }; - - await cloudFile.setMetadata(md); - - // Check that the tokens are saved in Firebase metadata - await supertest(STORAGE_EMULATOR_HOST) - .get(`/v0/b/${testBucket.name}/o/${encodeURIComponent(destination)}`) - .expect(200) - .then((res) => { - const firebaseMd = res.body; - expect(firebaseMd.downloadTokens).to.equal(md.metadata.firebaseStorageDownloadTokens); - }); - - // Check that the tokens are saved in Cloud metadata - const [metadata] = await cloudFile.getMetadata(); - expect(metadata.metadata.firebaseStorageDownloadTokens).to.deep.equal( - md.metadata.firebaseStorageDownloadTokens - ); - }); - }); - - describe("#setMetadata()", () => { - it("should throw on non-existing file", async () => { - let err: any; - await testBucket - .file(smallFilePath) - .setMetadata({ contentType: 9000 }) - .catch((_err) => { - err = _err; - }); - - expect(err).to.not.be.empty; - }); - - it("should allow overriding of default metadata", async () => { - await testBucket.upload(smallFilePath); - const [metadata] = await testBucket - .file(smallFilePath.split("/").slice(-1)[0]) - .setMetadata({ contentType: "very/fake" }); - - const metadataTypes: { [s: string]: string } = {}; - - for (const key in metadata) { - if (metadata[key]) { - metadataTypes[key] = typeof metadata[key]; - } - } - - expect(metadata.contentType).to.equal("very/fake"); - expect(metadataTypes).to.deep.equal({ - bucket: "string", - contentType: "string", - generation: "string", - md5Hash: "string", - crc32c: "string", - etag: "string", - metageneration: "string", - storageClass: "string", - name: "string", - size: "string", - timeCreated: "string", - updated: "string", - id: "string", - kind: "string", - mediaLink: "string", - selfLink: "string", - timeStorageClassUpdated: "string", - }); - }); - - it("should allow setting of optional metadata", async () => { - await testBucket.upload(smallFilePath); - const [metadata] = await testBucket - .file(smallFilePath.split("/").slice(-1)[0]) - .setMetadata({ cacheControl: "no-cache", contentLanguage: "en" }); - - const metadataTypes: { [s: string]: string } = {}; - - for (const key in metadata) { - if (metadata[key]) { - metadataTypes[key] = typeof metadata[key]; - } - } - - expect(metadata.cacheControl).to.equal("no-cache"); - expect(metadata.contentLanguage).to.equal("en"); - }); - - it("should allow fields under .metadata", async () => { - await testBucket.upload(smallFilePath); - const [metadata] = await testBucket - .file(smallFilePath.split("/").slice(-1)[0]) - .setMetadata({ metadata: { is_over: "9000" } }); - - expect(metadata.metadata.is_over).to.equal("9000"); - }); - - it("should ignore any unknown fields", async () => { - await testBucket.upload(smallFilePath); - const [metadata] = await testBucket - .file(smallFilePath.split("/").slice(-1)[0]) - .setMetadata({ nada: "true" }); - - expect(metadata.nada).to.be.undefined; - }); - }); - }); - - after(async () => { - if (tmpDir) { - fs.unlinkSync(smallFilePath); - fs.unlinkSync(largeFilePath); - fs.rmdirSync(tmpDir); - } - - if (!TEST_CONFIG.useProductionServers) { - delete process.env.STORAGE_EMULATOR_HOST; - await test.stopEmulators(); - } - }); - }); - - describe("Firebase Endpoints", () => { - let storage: Storage; - - const filename = "testing/storage_ref/image.png"; - - before(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - if (!TEST_CONFIG.useProductionServers) { - test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, emulatorConfig); - await test.startEmulators(["--only", "auth,storage"]); - } else { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, SERVICE_ACCOUNT_KEY); - storage = new Storage(); - } - - browser = await puppeteer.launch({ - headless: !TEST_CONFIG.showBrowser, - devtools: true, - }); - }); - - beforeEach(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - page = await browser.newPage(); - await page.goto("https://example.com", { waitUntil: "networkidle2" }); - - await page.addScriptTag({ - url: "https://www.gstatic.com/firebasejs/7.24.0/firebase-app.js", - }); - await page.addScriptTag({ - url: "https://www.gstatic.com/firebasejs/7.24.0/firebase-auth.js", - }); - // url: "https://storage.googleapis.com/fir-tools-builds/firebase-storage-new.js", - await page.addScriptTag({ - url: TEST_CONFIG.useProductionServers - ? "https://www.gstatic.com/firebasejs/7.24.0/firebase-storage.js" - : "https://storage.googleapis.com/fir-tools-builds/firebase-storage.js", - }); - - await page.evaluate( - (appConfig, useProductionServers, emulatorHost) => { - firebase.initializeApp(appConfig); - // Wiring the app to use either the auth emulator or production auth - // based on the config flag. - const auth = firebase.auth(); - if (!useProductionServers) { - auth.useEmulator(emulatorHost); - } - (window as any).auth = auth; - }, - appConfig, - TEST_CONFIG.useProductionServers, - AUTH_EMULATOR_HOST - ); - - if (!TEST_CONFIG.useProductionServers) { - await page.evaluate((hostAndPort) => { - const [host, port] = hostAndPort.split(":") as string[]; - (firebase.storage() as any).useEmulator(host, port); - }, STORAGE_EMULATOR_HOST.replace(/^(https?:|)\/\//, "")); - } - }); - - it("should upload a file", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - const uploadState = await page.evaluate((IMAGE_FILE_BASE64) => { - const auth = (window as any).auth as firebase.auth.Auth; - - return auth - .signInAnonymously() - .then(() => { - return firebase - .storage() - .ref("testing/image.png") - .putString(IMAGE_FILE_BASE64, "base64"); - }) - .then((task) => { - return task.state; - }) - .catch((err) => { - throw err.message; - }); - }, IMAGE_FILE_BASE64); - - expect(uploadState).to.equal("success"); - }); - - it("should upload replace existing file", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - const uploadText = (text: string) => - page.evaluate((TEXT_FILE) => { - const auth = (window as any).auth as firebase.auth.Auth; - - return auth - .signInAnonymously() - .then(() => { - return firebase.storage().ref("replace.txt").putString(TEXT_FILE); - }) - .then((task) => { - return task.state; - }) - .catch((err) => { - throw err.message; - }); - }, text); - - await uploadText("some-content"); - await uploadText("some-other-content"); - - const downloadUrl = await page.evaluate((filename) => { - return firebase.storage().ref("replace.txt").getDownloadURL(); - }, filename); - - const requestClient = TEST_CONFIG.useProductionServers ? https : http; - await new Promise((resolve, reject) => { - requestClient.get( - downloadUrl, - { - headers: { - // This is considered an authorized request in the emulator - Authorization: "Bearer owner", - }, - }, - (response) => { - const data: any = []; - response - .on("data", (chunk) => data.push(chunk)) - .on("end", () => { - expect(Buffer.concat(data).toString()).to.equal("some-other-content"); - }) - .on("close", resolve) - .on("error", reject); - } - ); - }); - }); - - it("should upload a file into a directory", async () => { - const uploadState = await page.evaluate((IMAGE_FILE_BASE64) => { - const auth = (window as any).auth as firebase.auth.Auth; - - return auth - .signInAnonymously() - .then(() => { - return firebase - .storage() - .ref("testing/storage_ref/big/path/image.png") - .putString(IMAGE_FILE_BASE64, "base64"); - }) - .then((task) => { - return task.state; - }) - .catch((err) => { - throw err.message; - }); - }, IMAGE_FILE_BASE64); - - expect(uploadState).to.equal("success"); - }); - - it("should upload a file using put", async () => { - const uploadState = await page.evaluate((IMAGE_FILE_BASE64) => { - const auth = (window as any).auth as firebase.auth.Auth; - const _file = new File([IMAGE_FILE_BASE64], "toUpload.txt"); - return auth - .signInAnonymously() - .then(() => { - return firebase.storage().ref("image_put.png").put(_file); - }) - .then((task) => { - return task.state; - }) - .catch((err) => { - throw err.message; - }); - }, IMAGE_FILE_BASE64); - - expect(uploadState).to.equal("success"); - }); - - describe(".ref()", () => { - beforeEach(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - if (!TEST_CONFIG.useProductionServers) { - await resetStorageEmulator(STORAGE_EMULATOR_HOST); - } else { - await storage.bucket(storageBucket).deleteFiles(); - } - - await page.evaluate( - (IMAGE_FILE_BASE64, filename) => { - const auth = (window as any).auth as firebase.auth.Auth; - - return auth - .signInAnonymously() - .then(() => { - return firebase.storage().ref(filename).putString(IMAGE_FILE_BASE64, "base64"); - }) - .then((task) => { - return task.state; - }) - .catch((err) => { - throw err.message; - }); - }, - IMAGE_FILE_BASE64, - filename - ); - }); - - describe("#listAll()", () => { - beforeEach(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - const refs = [ - "testing/storage_ref/image.png", - "testing/somePathEndsWithDoubleSlash//file.png", - ]; - for (const ref of refs) { - await page.evaluate( - async (IMAGE_FILE_BASE64, filename) => { - const auth = (window as any).auth as firebase.auth.Auth; - - try { - await auth.signInAnonymously(); - const task = await firebase - .storage() - .ref(filename) - .putString(IMAGE_FILE_BASE64, "base64"); - return task.state; - } catch (err) { - throw err.message; - } - }, - IMAGE_FILE_BASE64, - ref - ); - } - }); - - it("should list all files and prefixes", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - const itemNames = [...Array(5)].map((_, i) => `item#${i}`); - for (const item of itemNames) { - await page.evaluate( - async (IMAGE_FILE_BASE64, filename) => { - const auth = (window as any).auth as firebase.auth.Auth; - - try { - await auth.signInAnonymously(); - const task = await firebase - .storage() - .ref(filename) - .putString(IMAGE_FILE_BASE64, "base64"); - return task.state; - } catch (err) { - throw err.message; - } - }, - IMAGE_FILE_BASE64, - `testing/${item}` - ); - } - - const listResult = await page.evaluate(() => { - return firebase - .storage() - .ref("testing") - .listAll() - .then((list) => { - return { - prefixes: list.prefixes.map((prefix) => prefix.name), - items: list.items.map((item) => item.name), - }; - }); - }); - - expect(listResult).to.deep.equal({ - items: itemNames, - prefixes: ["somePathEndsWithDoubleSlash", "storage_ref"], - }); - }); - - it("should list implicit prefixes", async () => { - await page.evaluate( - async (IMAGE_FILE_BASE64, filename) => { - try { - await firebase.auth().signInAnonymously(); - const task = await firebase - .storage() - .ref(filename) - .putString(IMAGE_FILE_BASE64, "base64"); - return task.state; - } catch (err) { - throw err.message; - } - }, - IMAGE_FILE_BASE64, - `testing/implicit/deep/path/file.jpg` - ); - - const listResult = await page.evaluate(() => { - return firebase - .storage() - .ref("testing/implicit") - .listAll() - .then((list) => { - return { - prefixes: list.prefixes.map((prefix) => prefix.name), - items: list.items.map((item) => item.name), - }; - }); - }); - - expect(listResult).to.deep.equal({ - prefixes: ["deep"], - items: [], - }); - }); - - it("should list at /", async () => { - await page.evaluate( - async (IMAGE_FILE_BASE64, filename) => { - const auth = (window as any).auth as firebase.auth.Auth; - try { - await auth.signInAnonymously(); - const task = await firebase - .storage() - .ref(filename) - .putString(IMAGE_FILE_BASE64, "base64"); - return task.state; - } catch (err) { - throw err.message; - } - }, - IMAGE_FILE_BASE64, - `file.jpg` - ); - - const listResult = await page.evaluate(() => { - return firebase - .storage() - .ref() - .listAll() - .then((list) => { - return { - prefixes: list.prefixes.map((prefix) => prefix.name), - items: list.items.map((item) => item.name), - }; - }); - }); - - expect(listResult).to.deep.equal({ - prefixes: ["testing"], - items: ["file.jpg"], - }); - }); - }); - - describe("#list()", () => { - const itemNames = [...Array(10)].map((_, i) => `item#${i}`); - - beforeEach(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - for (const item of itemNames) { - await page.evaluate( - async (IMAGE_FILE_BASE64, filename) => { - const auth = (window as any).auth as firebase.auth.Auth; - - try { - await auth.signInAnonymously(); - const task = await firebase - .storage() - .ref(filename) - .putString(IMAGE_FILE_BASE64, "base64"); - return task.state; - } catch (err) { - throw err.message; - } - }, - IMAGE_FILE_BASE64, - `testing/list/${item}` - ); - } - }); - - it("should list only maxResults items with nextPageToken, when maxResults is set", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - const listItems = await page.evaluate(() => { - return firebase - .storage() - .ref("testing/list") - .list({ - maxResults: 4, - }) - .then((list) => { - return { - items: list.items.map((item) => item.name), - nextPageToken: list.nextPageToken, - }; - }); - }); - - expect(listItems.items).to.have.lengthOf(4); - expect(itemNames).to.include.members(listItems.items); - expect(listItems.nextPageToken).to.not.be.empty; - }); - - it("should paginate when nextPageToken is provided", async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - let responses: string[] = []; - let pageToken = ""; - let pageCount = 0; - - do { - const listResponse = await page.evaluate((pageToken) => { - return firebase - .storage() - .ref("testing/list") - .list({ - maxResults: 4, - pageToken, - }) - .then((list) => { - return { - items: list.items.map((item) => item.name), - nextPageToken: list.nextPageToken ?? "", - }; - }); - }, pageToken); - - responses = [...responses, ...listResponse.items]; - pageToken = listResponse.nextPageToken; - pageCount++; - - if (!listResponse.nextPageToken) { - expect(responses.sort()).to.deep.equal(itemNames); - expect(pageCount).to.be.equal(3); - break; - } - } while (true); - }); - }); - - it("updateMetadata throws on non-existent file", async () => { - const err = await page.evaluate(() => { - return firebase - .storage() - .ref("testing/thisFileDoesntExist") - .updateMetadata({ - contentType: "application/awesome-stream", - customMetadata: { - testable: "true", - }, - }) - .catch((_err) => { - return _err; - }); - }); - - expect(err).to.not.be.empty; - }); - - it("updateMetadata updates metadata successfully", async () => { - const metadata = await page.evaluate((filename) => { - return firebase - .storage() - .ref(filename) - .updateMetadata({ - contentType: "application/awesome-stream", - customMetadata: { - testable: "true", - }, - }); - }, filename); - - expect(metadata.contentType).to.equal("application/awesome-stream"); - expect(metadata.customMetadata.testable).to.equal("true"); - }); - - describe("#getDownloadURL()", () => { - it("returns url pointing to the expected host", async () => { - let downloadUrl; - try { - downloadUrl = await page.evaluate((filename) => { - return firebase.storage().ref(filename).getDownloadURL(); - }, filename); - } catch (err) { - expect(err).to.equal(""); - } - const expectedHost = TEST_CONFIG.useProductionServers - ? "https://firebasestorage.googleapis.com" - : STORAGE_EMULATOR_HOST; - - expect(downloadUrl).to.contain( - `${expectedHost}/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2Fimage.png?alt=media&token=` - ); - }); - - it("serves the right content", async () => { - const downloadUrl = await page.evaluate((filename) => { - return firebase.storage().ref(filename).getDownloadURL(); - }, filename); - - const requestClient = TEST_CONFIG.useProductionServers ? https : http; - await new Promise((resolve, reject) => { - requestClient.get( - downloadUrl, - { - headers: { - // This is considered an authorized request in the emulator - Authorization: "Bearer owner", - }, - }, - (response) => { - const data: any = []; - response - .on("data", (chunk) => data.push(chunk)) - .on("end", () => { - expect(Buffer.concat(data)).to.deep.equal( - Buffer.from(IMAGE_FILE_BASE64, "base64") - ); - }) - .on("close", resolve) - .on("error", reject); - } - ); - }); - }); - }); - - it("#getMetadata()", async () => { - const metadata = await page.evaluate((filename) => { - return firebase.storage().ref(filename).getMetadata(); - }, filename); - - const metadataTypes: { [s: string]: string } = {}; - - for (const key in metadata) { - if (metadata[key]) { - metadataTypes[key] = typeof metadata[key]; - } - } - - expect(metadataTypes).to.deep.equal({ - bucket: "string", - contentDisposition: "string", - contentEncoding: "string", - contentType: "string", - fullPath: "string", - generation: "string", - md5Hash: "string", - metageneration: "string", - name: "string", - size: "number", - timeCreated: "string", - type: "string", - updated: "string", - }); - }); - - describe("#setMetadata()", () => { - it("should allow for custom metadata to be set", async () => { - const metadata = await page.evaluate((filename) => { - return firebase - .storage() - .ref(filename) - .updateMetadata({ - customMetadata: { - is_over: "9000", - }, - }) - .then(() => { - return firebase.storage().ref(filename).getMetadata(); - }); - }, filename); - - expect(metadata.customMetadata.is_over).to.equal("9000"); - }); - - it("should allow deletion of custom metadata by setting to null", async () => { - const setMetadata = await page.evaluate((filename) => { - const storageReference = firebase.storage().ref(filename); - return storageReference.updateMetadata({ - contentType: "text/plain", - customMetadata: { - removeMe: "please", - }, - }); - }, filename); - - expect(setMetadata.customMetadata.removeMe).to.equal("please"); - - const nulledMetadata = await page.evaluate((filename) => { - const storageReference = firebase.storage().ref(filename); - return storageReference.updateMetadata({ - contentType: "text/plain", - customMetadata: { - removeMe: null as any, - }, - }); - }, filename); - - expect(nulledMetadata.customMetadata.removeMe).to.equal(undefined); - }); - }); - - it("#delete()", async () => { - const downloadUrl = await page.evaluate((filename) => { - return firebase.storage().ref(filename).getDownloadURL(); - }, filename); - - expect(downloadUrl).to.be.not.null; - - await page.evaluate((filename) => { - return firebase.storage().ref(filename).delete(); - }, filename); - - const error = await page.evaluate((filename) => { - return new Promise((resolve) => { - firebase - .storage() - .ref(filename) - .getDownloadURL() - .catch((err) => { - resolve(err.message); - }); - }); - }, filename); - - expect(error).to.contain("does not exist."); - }); - }); - - emulatorSpecificDescribe("Non-SDK Endpoints", () => { - beforeEach(async () => { - if (!TEST_CONFIG.useProductionServers) { - await resetStorageEmulator(STORAGE_EMULATOR_HOST); - } else { - await storage.bucket(storageBucket).deleteFiles(); - } - - await page.evaluate( - (IMAGE_FILE_BASE64, filename) => { - const auth = (window as any).auth as firebase.auth.Auth; - - return auth - .signInAnonymously() - .then(() => { - return firebase.storage().ref(filename).putString(IMAGE_FILE_BASE64, "base64"); - }) - .then((task) => { - return task.state; - }) - .catch((err) => { - throw err.message; - }); - }, - IMAGE_FILE_BASE64, - filename - ); - }); - - it("#addToken", async () => { - await supertest(STORAGE_EMULATOR_HOST) - .post(`/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2Fimage.png?create_token=true`) - .set({ Authorization: "Bearer owner" }) - .expect(200) - .then((res) => { - const md = res.body; - expect(md.downloadTokens.split(",").length).to.deep.equal(2); - }); - }); - - it("#addTokenWithBadParamIsBadRequest", async () => { - await supertest(STORAGE_EMULATOR_HOST) - .post( - `/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2Fimage.png?create_token=someNonTrueParam` - ) - .set({ Authorization: "Bearer owner" }) - .expect(400); - }); - - it("#deleteToken", async () => { - const tokens = await supertest(STORAGE_EMULATOR_HOST) - .post(`/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2Fimage.png?create_token=true`) - .set({ Authorization: "Bearer owner" }) - .expect(200) - .then((res) => { - const md = res.body; - const tokens = md.downloadTokens.split(","); - expect(tokens.length).to.equal(2); - - return tokens; - }); - // delete the newly added token - await supertest(STORAGE_EMULATOR_HOST) - .post( - `/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2Fimage.png?delete_token=${tokens[0]}` - ) - .set({ Authorization: "Bearer owner" }) - .expect(200) - .then((res) => { - const md = res.body; - expect(md.downloadTokens.split(",")).to.deep.equal([tokens[1]]); - }); - }); - - it("#deleteLastTokenStillLeavesOne", async () => { - const token = await supertest(STORAGE_EMULATOR_HOST) - .get(`/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2Fimage.png`) - .set({ Authorization: "Bearer owner" }) - .expect(200) - .then((res) => { - const md = res.body; - return md.downloadTokens; - }); - - // deleting the only token still generates one. - await supertest(STORAGE_EMULATOR_HOST) - .post(`/v0/b/${storageBucket}/o/testing%2Fstorage_ref%2Fimage.png?delete_token=${token}`) - .set({ Authorization: "Bearer owner" }) - .expect(200) - .then((res) => { - const md = res.body; - expect(md.downloadTokens.split(",").length).to.deep.equal(1); - expect(md.downloadTokens.split(",")).to.not.deep.equal([token]); - }); - }); - }); - - after(async function (this) { - this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); - - if (!TEST_CONFIG.keepBrowserOpen) { - await browser.close(); - } - if (!TEST_CONFIG.useProductionServers) { - await test.stopEmulators(); - } else { - delete process.env.GOOGLE_APPLICATION_CREDENTIALS; - } - }); - }); -}); diff --git a/scripts/storage-emulator-integration/utils.ts b/scripts/storage-emulator-integration/utils.ts new file mode 100644 index 00000000000..d8c69da09a7 --- /dev/null +++ b/scripts/storage-emulator-integration/utils.ts @@ -0,0 +1,97 @@ +import * as fs from "fs"; +import * as path from "path"; +import fetch from "node-fetch"; +import * as crypto from "crypto"; +import * as os from "os"; +import { FrameworkOptions } from "../integration-helpers/framework"; +const { google } = require("googleapis"); + +/* Various delays needed when integration test spawns parallel emulator subprocesses. */ +export const TEST_SETUP_TIMEOUT = 60000; +export const EMULATORS_SHUTDOWN_DELAY_MS = 5000; +export const SMALL_FILE_SIZE = 200 * 1024; /* 200 kB */ +// Firebase Emulator config, for starting up emulators +export const FIREBASE_EMULATOR_CONFIG = "firebase.json"; + +export function readEmulatorConfig(config = FIREBASE_EMULATOR_CONFIG): FrameworkOptions { + try { + return readJson(config); + } catch (error) { + throw new Error( + `Cannot read the emulator config. Please ensure that the file ${config} is present in the current directory.`, + ); + } +} + +export function getStorageEmulatorHost(emulatorConfig: FrameworkOptions) { + const port = emulatorConfig.emulators?.storage?.port; + if (port) { + return `http://127.0.0.1:${port}`; + } + throw new Error("Storage emulator config not found or invalid"); +} + +export function getAuthEmulatorHost(emulatorConfig: FrameworkOptions) { + const port = emulatorConfig.emulators?.auth?.port; + if (port) { + return `http://127.0.0.1:${port}`; + } + throw new Error("Auth emulator config not found or invalid"); +} + +/** + * Reads a JSON file in the current directory. + * + * @param filename name of the JSON file to be read. Must be in the current directory. + */ +export function readJson(filename: string) { + return JSON.parse(readFile(filename)); +} + +export function readAbsoluteJson(filename: string) { + return JSON.parse(readAbsoluteFile(filename)); +} + +export function readFile(filename: string): string { + const fullPath = path.join(__dirname, filename); + return readAbsoluteFile(fullPath); +} +export function readAbsoluteFile(filename: string): string { + if (!fs.existsSync(filename)) { + throw new Error(`Can't find file at ${filename}`); + } + return fs.readFileSync(filename, "utf8"); +} + +export function getTmpDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "storage-files")); +} + +export function createRandomFile(filename: string, sizeInBytes: number, tmpDir: string): string { + return writeToFile(filename, crypto.randomBytes(sizeInBytes), tmpDir); +} + +export function writeToFile(filename: string, contents: Buffer, tmpDir: string): string { + const fullPath = path.join(tmpDir, filename); + fs.writeFileSync(fullPath, contents); + return fullPath; +} + +/** + * Resets the storage layer of the Storage Emulator. + */ +export async function resetStorageEmulator(emulatorHost: string) { + await fetch(`${emulatorHost}/internal/reset`, { method: "POST" }); +} + +export async function getProdAccessToken(serviceAccountKey: any): Promise { + const jwtClient = new google.auth.JWT( + serviceAccountKey.client_email, + null, + serviceAccountKey.private_key, + ["https://www.googleapis.com/auth/cloud-platform"], + null, + ); + const credentials = await jwtClient.authorize(); + return credentials.access_token!; +} diff --git a/scripts/test-functions-config.js b/scripts/test-functions-config.js index 77393a7afff..4b0fc652c61 100644 --- a/scripts/test-functions-config.js +++ b/scripts/test-functions-config.js @@ -9,7 +9,7 @@ * - projectId defaults to `functions-integration-test` */ -var clc = require("cli-color"); +var clc = require("colorette"); var exec = require("child_process").exec; var execSync = require("child_process").execSync; var expect = require("chai").expect; @@ -18,7 +18,6 @@ var tmp = require("tmp"); var api = require("../lib/api"); var scopes = require("../lib/scopes"); -var { configstore } = require("../lib/configstore"); var projectId = process.argv[2] || "functions-integration-test"; var localFirebase = __dirname + "/../lib/bin/firebase.js"; @@ -29,7 +28,6 @@ var preTest = function () { var dir = tmp.dirSync({ prefix: "cfgtest_" }); tmpDir = dir.name; fs.copySync(projectDir, tmpDir); - api.setRefreshToken(configstore.get("tokens").refresh_token); api.setScopes(scopes.CLOUD_PLATFORM); execSync(`${localFirebase} functions:config:unset foo --project=${projectId}`, { cwd: tmpDir }); console.log("Done pretest prep."); @@ -48,7 +46,7 @@ var set = function (expression) { function (err) { expect(err).to.be.null; resolve(); - } + }, ); }); }; @@ -61,7 +59,7 @@ var unset = function (key) { function (err) { expect(err).to.be.null; resolve(); - } + }, ); }); }; @@ -74,7 +72,7 @@ var getAndCompare = function (expected) { function (err, stdout) { expect(JSON.parse(stdout)).to.deep.equal(expected); resolve(); - } + }, ); }); }; diff --git a/scripts/test-functions-deploy.js b/scripts/test-functions-deploy.js index f2e07102da7..9ae3e3f4249 100644 --- a/scripts/test-functions-deploy.js +++ b/scripts/test-functions-deploy.js @@ -22,8 +22,9 @@ var scopes = require("../lib/scopes"); var { configstore } = require("../lib/configstore"); var extractTriggers = require("../lib/deploy/functions/runtimes/node/extractTriggers"); var functionsConfig = require("../lib/functionsConfig"); +var { last } = require("../lib/utils"); -var clc = require("cli-color"); +var clc = require("colorette"); var firebase = require("firebase"); var functionsSource = __dirname + "/assets/functions_to_test.js"; @@ -37,7 +38,7 @@ var tmpDir; var app; var deleteAllFunctions = function () { - var toDelete = _.map(parseFunctionsList(), function (funcName) { + var toDelete = parseFunctionsList().map(function (funcName) { return funcName.replace("-", "."); }); return localFirebase + ` functions:delete ${toDelete.join(" ")} -f --project=${projectId}`; @@ -46,7 +47,7 @@ var deleteAllFunctions = function () { var parseFunctionsList = function () { var triggers = []; extractTriggers(require(functionsSource), triggers); - return _.map(triggers, "name"); + return triggers.map((t) => t.name); }; var getUuid = function () { @@ -97,7 +98,7 @@ var checkFunctionsListMatch = function (expectedFunctions) { return cloudfunctions .listFunctions(projectId, region) .then(function (result) { - deployedFunctions = _.map(result, (fn) => _.last(fn.name.split("/"))); + deployedFunctions = (result || []).map((fn) => last(fn.name.split("/"))); expect(_.isEmpty(_.xor(expectedFunctions, deployedFunctions))).to.be.true; return true; }) @@ -130,7 +131,7 @@ var testCreateUpdateWithFilter = function () { console.log(stdout); expect(err).to.be.null; resolve(checkFunctionsListMatch(["nested-dbAction", "httpsAction"])); - } + }, ); }); }; @@ -154,7 +155,7 @@ var testDeleteWithFilter = function () { console.log(stdout); expect(err).to.be.null; resolve(checkFunctionsListMatch(["httpsAction"])); - } + }, ); }); }; @@ -272,7 +273,7 @@ var testFunctionsTrigger = function () { return waitForAck(uuid, "storage triggered function"); }); var checkScheduleAction = triggerSchedule( - "firebase-schedule-pubsubScheduleAction-us-central1" + "firebase-schedule-pubsubScheduleAction-us-central1", ).then(function (/* uuid */) { return true; }); @@ -314,7 +315,7 @@ var main = function () { }) .then(function () { console.log( - clc.green("\u2713 Test passed: threw warning when passing filter with unknown identifier") + clc.green("\u2713 Test passed: threw warning when passing filter with unknown identifier"), ); }) .catch(function (err) { diff --git a/scripts/test-functions-env.js b/scripts/test-functions-env.js index 7b07e656b59..201fb09c954 100755 --- a/scripts/test-functions-env.js +++ b/scripts/test-functions-env.js @@ -93,12 +93,12 @@ async function runTest(description, envFiles, expected) { await runTest( "Inject environment variables from .env", { ".env": "FOO=foo\nBAR=bar\nCAR=car" }, - { FOO: "foo", BAR: "bar", CAR: "car" } + { FOO: "foo", BAR: "bar", CAR: "car" }, ); await runTest( "Inject environment variables from .env and .env.", { ".env": "FOO=foo\nSOURCE=env", [`.env.${projectId}`]: "SOURCE=env-project" }, - { FOO: "foo", SOURCE: "env-project" } + { FOO: "foo", SOURCE: "env-project" }, ); console.log("Success"); } catch (err) { diff --git a/scripts/test-project/functions/package.json b/scripts/test-project/functions/package.json index 4221129235a..e71721db05a 100644 --- a/scripts/test-project/functions/package.json +++ b/scripts/test-project/functions/package.json @@ -2,10 +2,10 @@ "name": "functions", "description": "Firebase Functions", "dependencies": { - "firebase-admin": "~8.6.0", - "firebase-functions": "^3.2.0" + "firebase-admin": "^12.0.0", + "firebase-functions": "^4.9.0" }, "engines": { - "node": "12" + "node": "20" } } diff --git a/scripts/triggers-end-to-end-tests/README.md b/scripts/triggers-end-to-end-tests/README.md index 77cf7899b37..e75619b254e 100644 --- a/scripts/triggers-end-to-end-tests/README.md +++ b/scripts/triggers-end-to-end-tests/README.md @@ -8,16 +8,16 @@ introduced in the following PRs: # Running Instructions -Install dependencies: +From the firebase-tools folder, install dependencies: ``` -cd firebase-tools/scripts/triggers-end-to-end-tests && npm install +$ (cd scripts/triggers-end-to-end-tests && npm install) ``` Run the test: ``` -$ cd firebase-tools/scripts/triggers-end-to-end-tests && npm test +$ FBTOOLS_TARGET_PROJECT=demo-test npm run test:triggers-end-to-end ``` This end-to-end test uses the mocha testing framework. diff --git a/scripts/triggers-end-to-end-tests/firebase.json b/scripts/triggers-end-to-end-tests/firebase.json index 4a18a29f2ba..e37ee130128 100644 --- a/scripts/triggers-end-to-end-tests/firebase.json +++ b/scripts/triggers-end-to-end-tests/firebase.json @@ -6,7 +6,21 @@ "storage": { "rules": "storage.rules" }, - "functions": {}, + "functions": [ + { + "codebase": "triggers", + "source": "triggers" + }, + { + "codebase": "v1", + "source": "v1" + }, + { + "codebase": "v2", + "source": "v2" + } + + ], "emulators": { "hub": { "port": 4000 diff --git a/scripts/triggers-end-to-end-tests/functions/index.js b/scripts/triggers-end-to-end-tests/functions/index.js deleted file mode 100644 index 9778e546674..00000000000 --- a/scripts/triggers-end-to-end-tests/functions/index.js +++ /dev/null @@ -1,337 +0,0 @@ -const admin = require("firebase-admin"); -const functions = require("firebase-functions"); -let functionsV2; -try { - functionsV2 = require("firebase-functions/v2"); -} catch { - // TODO: firebase-functions/lib path is unsupported, but this is the only way to access the v2 namespace in Node 10. - // Remove this ugly hack once we cut support for Node 10. - functionsV2 = require("firebase-functions/lib/v2"); -} -const { PubSub } = require("@google-cloud/pubsub"); - -/* - * Log snippets that the driver program above checks for. Be sure to update - * ../test.js if you plan on changing these. - */ -/* Functions V1 */ -const RTDB_FUNCTION_LOG = "========== RTDB FUNCTION =========="; -const FIRESTORE_FUNCTION_LOG = "========== FIRESTORE FUNCTION =========="; -const PUBSUB_FUNCTION_LOG = "========== PUBSUB FUNCTION =========="; -const AUTH_FUNCTION_LOG = "========== AUTH FUNCTION =========="; -const STORAGE_FUNCTION_ARCHIVED_LOG = "========== STORAGE FUNCTION ARCHIVED =========="; -const STORAGE_FUNCTION_DELETED_LOG = "========== STORAGE FUNCTION DELETED =========="; -const STORAGE_FUNCTION_FINALIZED_LOG = "========== STORAGE FUNCTION FINALIZED =========="; -const STORAGE_FUNCTION_METADATA_LOG = "========== STORAGE FUNCTION METADATA =========="; -const STORAGE_BUCKET_FUNCTION_ARCHIVED_LOG = - "========== STORAGE BUCKET FUNCTION ARCHIVED =========="; -const STORAGE_BUCKET_FUNCTION_DELETED_LOG = "========== STORAGE BUCKET FUNCTION DELETED =========="; -const STORAGE_BUCKET_FUNCTION_FINALIZED_LOG = - "========== STORAGE BUCKET FUNCTION FINALIZED =========="; -const STORAGE_BUCKET_FUNCTION_METADATA_LOG = - "========== STORAGE BUCKET FUNCTION METADATA =========="; -/* Functions V2 */ -const PUBSUB_FUNCTION_V2_LOG = "========== PUBSUB V2 FUNCTION =========="; -const STORAGE_FUNCTION_V2_ARCHIVED_LOG = "========== STORAGE V2 FUNCTION ARCHIVED =========="; -const STORAGE_FUNCTION_V2_DELETED_LOG = "========== STORAGE V2 FUNCTION DELETED =========="; -const STORAGE_FUNCTION_V2_FINALIZED_LOG = "========== STORAGE V2 FUNCTION FINALIZED =========="; -const STORAGE_FUNCTION_V2_METADATA_LOG = "========== STORAGE V2 FUNCTION METADATA =========="; -const STORAGE_BUCKET_FUNCTION_V2_ARCHIVED_LOG = - "========== STORAGE BUCKET V2 FUNCTION ARCHIVED =========="; -const STORAGE_BUCKET_FUNCTION_V2_DELETED_LOG = - "========== STORAGE BUCKET V2 FUNCTION DELETED =========="; -const STORAGE_BUCKET_FUNCTION_V2_FINALIZED_LOG = - "========== STORAGE BUCKET V2 FUNCTION FINALIZED =========="; -const STORAGE_BUCKET_FUNCTION_V2_METADATA_LOG = - "========== STORAGE BUCKET V2 FUNCTION METADATA =========="; - -/* - * We install onWrite triggers for START_DOCUMENT_NAME in both the firestore and - * database emulators. From each respective onWrite trigger, we write a document - * to both the firestore and database emulators. This exercises the - * bidirectional communication between cloud functions and each emulator. - */ -const START_DOCUMENT_NAME = "test/start"; -const END_DOCUMENT_NAME = "test/done"; - -const PUBSUB_TOPIC = "test-topic"; -const PUBSUB_SCHEDULED_TOPIC = "firebase-schedule-pubsubScheduled"; - -const STORAGE_FILE_NAME = "test-file.txt"; - -const pubsub = new PubSub(); -admin.initializeApp(); - -exports.deleteFromFirestore = functions.https.onRequest(async (req, res) => { - await admin.firestore().doc(START_DOCUMENT_NAME).delete(); - res.json({ deleted: true }); -}); - -exports.deleteFromRtdb = functions.https.onRequest(async (req, res) => { - await admin.database().ref(START_DOCUMENT_NAME).remove(); - res.json({ deleted: true }); -}); - -exports.writeToFirestore = functions.https.onRequest(async (req, res) => { - const ref = admin.firestore().doc(START_DOCUMENT_NAME); - await ref.set({ start: new Date().toISOString() }); - ref.get().then((snap) => { - res.json({ data: snap.data() }); - }); -}); - -exports.writeToRtdb = functions.https.onRequest(async (req, res) => { - const ref = admin.database().ref(START_DOCUMENT_NAME); - await ref.set({ start: new Date().toISOString() }); - ref.once("value", (snap) => { - res.json({ data: snap }); - }); -}); - -exports.writeToPubsub = functions.https.onRequest(async (req, res) => { - const msg = await pubsub.topic(PUBSUB_TOPIC).publishJSON({ foo: "bar" }, { attr: "val" }); - console.log("PubSub Emulator Host", process.env.PUBSUB_EMULATOR_HOST); - console.log("Wrote PubSub Message", msg); - res.json({ published: "ok" }); -}); - -exports.writeToScheduledPubsub = functions.https.onRequest(async (req, res) => { - const msg = await pubsub - .topic(PUBSUB_SCHEDULED_TOPIC) - .publishJSON({ foo: "bar" }, { attr: "val" }); - console.log("PubSub Emulator Host", process.env.PUBSUB_EMULATOR_HOST); - console.log("Wrote Scheduled PubSub Message", msg); - res.json({ published: "ok" }); -}); - -exports.writeToAuth = functions.https.onRequest(async (req, res) => { - const time = new Date().getTime(); - await admin.auth().createUser({ - uid: `uid${time}`, - email: `user${time}@example.com`, - }); - - res.json({ created: "ok" }); -}); - -exports.writeToDefaultStorage = functions.https.onRequest(async (req, res) => { - await admin.storage().bucket().file(STORAGE_FILE_NAME).save("hello world!"); - console.log("Wrote to default Storage bucket"); - res.json({ created: "ok" }); -}); - -exports.writeToSpecificStorageBucket = functions.https.onRequest(async (req, res) => { - await admin.storage().bucket("test-bucket").file(STORAGE_FILE_NAME).save("hello world!"); - console.log("Wrote to a specific Storage bucket"); - res.json({ created: "ok" }); -}); - -exports.updateDeleteFromDefaultStorage = functions.https.onRequest(async (req, res) => { - await admin.storage().bucket().file(STORAGE_FILE_NAME).save("something new!"); - console.log("Wrote to Storage bucket"); - await admin.storage().bucket().file(STORAGE_FILE_NAME).delete(); - console.log("Deleted from Storage bucket"); - res.json({ done: "ok" }); -}); - -exports.updateDeleteFromSpecificStorageBucket = functions.https.onRequest(async (req, res) => { - await admin.storage().bucket("test-bucket").file(STORAGE_FILE_NAME).save("something new!"); - console.log("Wrote to a specific Storage bucket"); - await admin.storage().bucket("test-bucket").file(STORAGE_FILE_NAME).delete(); - console.log("Deleted from a specific Storage bucket"); - res.json({ done: "ok" }); -}); - -exports.firestoreReaction = functions.firestore - .document(START_DOCUMENT_NAME) - .onWrite(async (/* change, ctx */) => { - console.log(FIRESTORE_FUNCTION_LOG); - /* - * Write back a completion timestamp to the firestore emulator. The test - * driver program checks for this by querying the firestore emulator - * directly. - */ - const ref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_firestore"); - await ref.set({ done: new Date().toISOString() }); - - /* - * Write a completion marker to the firestore emulator. This exercise - * cross-emulator communication. - */ - const dbref = admin.database().ref(END_DOCUMENT_NAME + "_from_firestore"); - await dbref.set({ done: new Date().toISOString() }); - - return true; - }); - -exports.rtdbReaction = functions.database - .ref(START_DOCUMENT_NAME) - .onWrite(async (/* change, ctx */) => { - console.log(RTDB_FUNCTION_LOG); - - const ref = admin.database().ref(END_DOCUMENT_NAME + "_from_database"); - await ref.set({ done: new Date().toISOString() }); - - const firestoreref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_database"); - await firestoreref.set({ done: new Date().toISOString() }); - - return true; - }); - -exports.pubsubReaction = functions.pubsub.topic(PUBSUB_TOPIC).onPublish((msg /* , ctx */) => { - console.log(PUBSUB_FUNCTION_LOG); - console.log("Message", JSON.stringify(msg.json)); - console.log("Attributes", JSON.stringify(msg.attributes)); - return true; -}); - -exports.pubsubv2reaction = functionsV2.pubsub.onMessagePublished(PUBSUB_TOPIC, (cloudevent) => { - console.log(PUBSUB_FUNCTION_V2_LOG); - console.log("Message", JSON.stringify(cloudevent.data.message.json)); - console.log("Attributes", JSON.stringify(cloudevent.data.message.attributes)); - return true; -}); - -exports.pubsubScheduled = functions.pubsub.schedule("every mon 07:00").onRun((context) => { - console.log(PUBSUB_FUNCTION_LOG); - console.log("Resource", JSON.stringify(context.resource)); - return true; -}); - -exports.authReaction = functions.auth.user().onCreate((user, ctx) => { - console.log(AUTH_FUNCTION_LOG); - console.log("User", JSON.stringify(user)); - return true; -}); - -exports.storageArchiveReaction = functions.storage - .bucket() - .object() - .onArchive((object, context) => { - console.log(STORAGE_FUNCTION_ARCHIVED_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storageDeleteReaction = functions.storage - .bucket() - .object() - .onDelete((object, context) => { - console.log(STORAGE_FUNCTION_DELETED_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storageFinalizeReaction = functions.storage - .bucket() - .object() - .onFinalize((object, context) => { - console.log(STORAGE_FUNCTION_FINALIZED_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storageMetadataReaction = functions.storage - .bucket() - .object() - .onMetadataUpdate((object, context) => { - console.log(STORAGE_FUNCTION_METADATA_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storagev2archivedreaction = functionsV2.storage.onObjectArchived((cloudevent) => { - console.log(STORAGE_FUNCTION_V2_ARCHIVED_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; -}); - -exports.storagev2deletedreaction = functionsV2.storage.onObjectDeleted((cloudevent) => { - console.log(STORAGE_FUNCTION_V2_DELETED_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; -}); - -exports.storagev2finalizedreaction = functionsV2.storage.onObjectFinalized((cloudevent) => { - console.log(STORAGE_FUNCTION_V2_FINALIZED_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; -}); - -exports.storagev2metadatareaction = functionsV2.storage.onObjectMetadataUpdated((cloudevent) => { - console.log(STORAGE_FUNCTION_V2_METADATA_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; -}); - -exports.storageBucketArchiveReaction = functions.storage - .bucket("test-bucket") - .object() - .onArchive((object, context) => { - console.log(STORAGE_BUCKET_FUNCTION_ARCHIVED_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storageBucketDeleteReaction = functions.storage - .bucket("test-bucket") - .object() - .onDelete((object, context) => { - console.log(STORAGE_BUCKET_FUNCTION_DELETED_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storageBucketFinalizeReaction = functions.storage - .bucket("test-bucket") - .object() - .onFinalize((object, context) => { - console.log(STORAGE_BUCKET_FUNCTION_FINALIZED_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storageBucketMetadataReaction = functions.storage - .bucket("test-bucket") - .object() - .onMetadataUpdate((object, context) => { - console.log(STORAGE_BUCKET_FUNCTION_METADATA_LOG); - console.log("Object", JSON.stringify(object)); - return true; - }); - -exports.storagebucketv2archivedreaction = functionsV2.storage.onObjectArchived( - "test-bucket", - (cloudevent) => { - console.log(STORAGE_BUCKET_FUNCTION_V2_ARCHIVED_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; - } -); - -exports.storagebucketv2deletedreaction = functionsV2.storage.onObjectDeleted( - "test-bucket", - (cloudevent) => { - console.log(STORAGE_BUCKET_FUNCTION_V2_DELETED_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; - } -); - -exports.storagebucketv2finalizedreaction = functionsV2.storage.onObjectFinalized( - "test-bucket", - (cloudevent) => { - console.log(STORAGE_BUCKET_FUNCTION_V2_FINALIZED_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; - } -); - -exports.storagebucketv2metadatareaction = functionsV2.storage.onObjectMetadataUpdated( - "test-bucket", - (cloudevent) => { - console.log(STORAGE_BUCKET_FUNCTION_V2_METADATA_LOG); - console.log("Object", JSON.stringify(cloudevent.data)); - return true; - } -); diff --git a/scripts/triggers-end-to-end-tests/functions/package.json b/scripts/triggers-end-to-end-tests/functions/package.json deleted file mode 100644 index 412c456b046..00000000000 --- a/scripts/triggers-end-to-end-tests/functions/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "functions", - "description": "Cloud Functions for Firebase", - "scripts": {}, - "engines": { - "node": "12" - }, - "dependencies": { - "@google-cloud/pubsub": "^1.1.5", - "firebase-admin": "^9.3.0", - "firebase-functions": "^3.16", - "@firebase/database-compat": "0.1.2" - }, - "devDependencies": { - "firebase-functions-test": "^0.2.0" - }, - "private": true -} diff --git a/scripts/triggers-end-to-end-tests/run.sh b/scripts/triggers-end-to-end-tests/run.sh index 2399b8996ba..b4ffcd00e83 100755 --- a/scripts/triggers-end-to-end-tests/run.sh +++ b/scripts/triggers-end-to-end-tests/run.sh @@ -1,18 +1,36 @@ #!/bin/bash -source scripts/set-default-credentials.sh -./scripts/npm-link.sh +function cleanup() { + if ! command -v lsof &> /dev/null + then + echo "lsof could not be found" + exit + fi + # Kill all emulator processes + for PORT in 4000 9000 9001 9002 8085 9099 9199 + do + PID=$(lsof -t -i:$PORT || true) + if [ -n "$PID" ] + then + kill -9 $PID + fi + done +} +trap cleanup EXIT -( - cd scripts/triggers-end-to-end-tests/functions - npm install -) +source scripts/set-default-credentials.sh +./scripts/clean-install.sh -npx mocha \ - --require ts-node/register \ - --require source-map-support/register \ - --require src/test/helpers/mocha-bootstrap.ts \ - --exit \ - scripts/triggers-end-to-end-tests/tests.ts +for dir in triggers v1 v2; do + ( + cd scripts/triggers-end-to-end-tests/$dir + npm install + ) +done -rm scripts/triggers-end-to-end-tests/functions/package.json +if [ "$1" == "inspect" ] +then + npx mocha --exit scripts/triggers-end-to-end-tests/tests.inspect.ts +else + npx mocha --exit scripts/triggers-end-to-end-tests/tests.ts +fi \ No newline at end of file diff --git a/scripts/triggers-end-to-end-tests/tests.inspect.ts b/scripts/triggers-end-to-end-tests/tests.inspect.ts new file mode 100755 index 00000000000..a25fe795d9b --- /dev/null +++ b/scripts/triggers-end-to-end-tests/tests.inspect.ts @@ -0,0 +1,96 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as path from "path"; + +import { FrameworkOptions, TriggerEndToEndTest } from "../integration-helpers/framework"; + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; +/* + * Various delays that are needed because this test spawns + * parallel emulator subprocesses. + */ +const TEST_SETUP_TIMEOUT = 80000; +const EMULATORS_WRITE_DELAY_MS = 5000; +const EMULATORS_SHUTDOWN_DELAY_MS = 5000; + +function readConfig(): FrameworkOptions { + const filename = path.join(__dirname, "firebase.json"); + const data = fs.readFileSync(filename, "utf8"); + return JSON.parse(data); +} + +describe("function triggers with inspect flag", () => { + let test: TriggerEndToEndTest; + + before(async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; + + const config = readConfig(); + test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); + await test.startEmulators(["--only", "functions,auth,storage", "--inspect-functions"]); + }); + + after(async function (this) { + this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); + await test.stopEmulators(); + }); + + describe("http functions", () => { + it("should invoke correct function in the same codebase", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const v1response = await test.invokeHttpFunction("onreqv2b"); + expect(v1response.status).to.equal(200); + const v1body = await v1response.text(); + expect(v1body).to.deep.equal("onreqv2b"); + + const v2response = await test.invokeHttpFunction("onreqv2a"); + expect(v2response.status).to.equal(200); + const v2body = await v2response.text(); + expect(v2body).to.deep.equal("onreqv2a"); + }); + + it("should invoke correct function across codebases", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const v1response = await test.invokeHttpFunction("onReq"); + expect(v1response.status).to.equal(200); + const v1body = await v1response.text(); + expect(v1body).to.deep.equal("onReq"); + + const v2response = await test.invokeHttpFunction("onreqv2a"); + expect(v2response.status).to.equal(200); + const v2body = await v2response.text(); + expect(v2body).to.deep.equal("onreqv2a"); + }); + + it("should disable timeout", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const v2response = await test.invokeHttpFunction("onreqv2timeout"); + expect(v2response.status).to.equal(200); + const v2body = await v2response.text(); + expect(v2body).to.deep.equal("onreqv2timeout"); + }); + }); + + describe("event triggered (multicast) functions", () => { + it("should trigger auth triggered functions in response to auth events", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const response = await test.writeToAuth(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + expect(test.authTriggerCount).to.equal(1); + }); + + it("should trigger storage triggered functions in response to storage events across codebases", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + const response = await test.writeToDefaultStorage(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + + expect(test.storageFinalizedTriggerCount).to.equal(1); + expect(test.storageV2FinalizedTriggerCount).to.equal(1); + }); + }); +}); diff --git a/scripts/triggers-end-to-end-tests/tests.ts b/scripts/triggers-end-to-end-tests/tests.ts old mode 100755 new mode 100644 index 8063f98f20f..fe00c9483d2 --- a/scripts/triggers-end-to-end-tests/tests.ts +++ b/scripts/triggers-end-to-end-tests/tests.ts @@ -2,10 +2,8 @@ import { expect } from "chai"; import * as admin from "firebase-admin"; import { Firestore } from "@google-cloud/firestore"; import * as fs from "fs"; -import * as os from "os"; import * as path from "path"; -import { CLIProcess } from "../integration-helpers/cli"; import { FrameworkOptions, TriggerEndToEndTest } from "../integration-helpers/framework"; const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; @@ -18,15 +16,13 @@ const ADMIN_CREDENTIAL = { }, }; -const ALL_EMULATORS_STARTED_LOG = "All emulators ready"; - /* * Various delays that are needed because this test spawns * parallel emulator subprocesses. */ -const TEST_SETUP_TIMEOUT = 60000; +const TEST_SETUP_TIMEOUT = 80000; const EMULATORS_WRITE_DELAY_MS = 5000; -const EMULATORS_SHUTDOWN_DELAY_MS = 5000; +const EMULATORS_SHUTDOWN_DELAY_MS = 7000; const EMULATOR_TEST_TIMEOUT = EMULATORS_WRITE_DELAY_MS * 2; /* @@ -42,16 +38,7 @@ function readConfig(): FrameworkOptions { return JSON.parse(data); } -function logIncludes(msg: string) { - return (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(msg); - }; -} - -describe("database and firestore emulator function triggers", () => { +describe("function triggers", () => { let test: TriggerEndToEndTest; let database: admin.database.Database | undefined; let firestore: admin.firestore.Firestore | undefined; @@ -64,18 +51,18 @@ describe("database and firestore emulator function triggers", () => { const config = readConfig(); test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); - await test.startEmulators(["--only", "functions,database,firestore"]); + await test.startEmulators(["--only", "functions,database,firestore,pubsub,storage,auth"]); firestore = new Firestore({ port: test.firestoreEmulatorPort, projectId: FIREBASE_PROJECT, - servicePath: "localhost", + servicePath: "127.0.0.1", ssl: false, }); admin.initializeApp({ projectId: FIREBASE_PROJECT, - databaseURL: `http://localhost:${test.rtdbEmulatorPort}?ns=${FIREBASE_PROJECT}`, + databaseURL: `http://127.0.0.1:${test.rtdbEmulatorPort}?ns=${FIREBASE_PROJECT}`, credential: ADMIN_CREDENTIAL, }); @@ -93,7 +80,7 @@ describe("database and firestore emulator function triggers", () => { }, (err: Error) => { expect.fail(err, `Error reading ${FIRESTORE_COMPLETION_MARKER} from database emulator.`); - } + }, ); database.ref(DATABASE_COMPLETION_MARKER).on( @@ -103,7 +90,7 @@ describe("database and firestore emulator function triggers", () => { }, (err: Error) => { expect.fail(err, `Error reading ${DATABASE_COMPLETION_MARKER} from database emulator.`); - } + }, ); let unsub = firestore.doc(FIRESTORE_COMPLETION_MARKER).onSnapshot( @@ -112,7 +99,7 @@ describe("database and firestore emulator function triggers", () => { }, (err: Error) => { expect.fail(err, `Error reading ${FIRESTORE_COMPLETION_MARKER} from firestore emulator.`); - } + }, ); firestoreUnsub.push(unsub); @@ -122,7 +109,7 @@ describe("database and firestore emulator function triggers", () => { }, (err: Error) => { expect.fail(err, `Error reading ${DATABASE_COMPLETION_MARKER} from firestore emulator.`); - } + }, ); firestoreUnsub.push(unsub); }); @@ -135,756 +122,378 @@ describe("database and firestore emulator function triggers", () => { await test.stopEmulators(); }); - it("should write to the database emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); - - const response = await test.writeToRtdb(); - expect(response.status).to.equal(200); - }); - - it("should write to the firestore emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); - - const response = await test.writeToFirestore(); - expect(response.status).to.equal(200); - - /* - * We delay again here because the functions triggered - * by the previous two writes run parallel to this and - * we need to give them and previous installed test - * fixture state handlers to complete before we check - * that state in the next test. - */ - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); - - it("should have have triggered cloud functions", () => { - expect(test.rtdbTriggerCount).to.equal(1); - expect(test.firestoreTriggerCount).to.equal(1); - /* - * Check for the presence of all expected documents in the firestore - * and database emulators. - */ - expect(test.success()).to.equal(true); - }); -}); - -describe("pubsub emulator function triggers", () => { - let test: TriggerEndToEndTest; - - before(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); - - expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; - - const config = readConfig(); - test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); - await test.startEmulators(["--only", "functions,pubsub"]); - }); - - after(async function (this) { - this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); - await test.stopEmulators(); - }); - - it("should write to the pubsub emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); + describe("https triggers", () => { + it("should handle parallel requests", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); - const response = await test.writeToPubsub(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); + const [resp1, resp2] = await Promise.all([ + test.invokeHttpFunction("httpsv2reaction"), + test.invokeHttpFunction("httpsv2reaction"), + ]); - it("should have have triggered cloud functions", () => { - expect(test.pubsubTriggerCount).to.equal(1); - expect(test.pubsubV2TriggerCount).to.equal(1); - }); - - it("should write to the scheduled pubsub emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); - - const response = await test.writeToScheduledPubsub(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); - - it("should have have triggered cloud functions", () => { - expect(test.pubsubTriggerCount).to.equal(2); + expect(resp1.status).to.eq(200); + expect(resp2.status).to.eq(200); + }); }); -}); -describe("auth emulator function triggers", () => { - let test: TriggerEndToEndTest; + describe("database and firestore emulator triggers", () => { + it("should write to the database emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - before(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); + const response = await test.writeToRtdb(); + expect(response.status).to.equal(200); + }); - expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; + it("should write to the firestore emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT * 2); - const config = readConfig(); - test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); - await test.startEmulators(["--only", "functions,auth"]); - }); + const response = await test.writeToFirestore(); + expect(response.status).to.equal(200); - after(async function (this) { - this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); - await test.stopEmulators(); - }); + /* + * We delay again here because the functions triggered + * by the previous two writes run parallel to this and + * we need to give them and previous installed test + * fixture state handlers to complete before we check + * that state in the next test. + */ + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS * 2)); + }); - it("should write to the auth emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); - const response = await test.writeToAuth(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + it("should have have triggered cloud functions", () => { + expect(test.rtdbTriggerCount).to.equal(1); + expect(test.rtdbV2TriggerCount).to.eq(1); + expect(test.firestoreTriggerCount).to.equal(1); + expect(test.firestoreV2TriggerCount).to.equal(1); + /* + * Check for the presence of all expected documents in the firestore + * and database emulators. + */ + expect(test.success()).to.equal(true); + }); }); - it("should have have triggered cloud functions", () => { - expect(test.authTriggerCount).to.equal(1); - }); -}); + describe("pubsub emulator triggered functions", () => { + it("should write to the pubsub emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); -describe("storage emulator function triggers", () => { - let test: TriggerEndToEndTest; + const response = await test.writeToPubsub(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - before(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); + it("should have have triggered cloud functions", () => { + expect(test.pubsubTriggerCount).to.equal(1); + expect(test.pubsubV2TriggerCount).to.equal(1); + }); - expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; + it("should write to the scheduled pubsub emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - const config = readConfig(); - test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); - await test.startEmulators(["--only", "functions,storage"]); - }); + const response = await test.writeToScheduledPubsub(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - after(async function (this) { - this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); - await test.stopEmulators(); + it("should have have triggered cloud functions", () => { + expect(test.pubsubTriggerCount).to.equal(2); + }); }); - it("should write to the default bucket of storage emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); + describe("auth emulator triggered functions", () => { + it("should write to the auth emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); + const response = await test.writeToAuth(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - const response = await test.writeToDefaultStorage(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); + it("should have have triggered cloud functions", () => { + expect(test.authTriggerCount).to.equal(1); + }); - it("should have triggered cloud functions", () => { - /* on object create two events fire (finalize & metadata update) */ - // default bucket - expect(test.storageFinalizedTriggerCount).to.equal(1); - expect(test.storageMetadataTriggerCount).to.equal(1); - expect(test.storageV2FinalizedTriggerCount).to.equal(1); - expect(test.storageV2MetadataTriggerCount).to.equal(1); - expect(test.storageDeletedTriggerCount).to.equal(0); - expect(test.storageV2DeletedTriggerCount).to.equal(0); - // specific bucket - expect(test.storageBucketFinalizedTriggerCount).to.equal(0); - expect(test.storageBucketMetadataTriggerCount).to.equal(0); - expect(test.storageBucketV2FinalizedTriggerCount).to.equal(0); - expect(test.storageBucketV2MetadataTriggerCount).to.equal(0); - expect(test.storageBucketDeletedTriggerCount).to.equal(0); - expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); - test.resetCounts(); - }); + it("should create a user in the auth emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT * 2); + const response = await test.createUserFromAuth(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - it("should write to a specific bucket of storage emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); + it("should have triggered cloud functions", () => { + expect(test.authBlockingCreateV2TriggerCount).to.equal(1); + // Creating a User also triggers the before sign in trigger + expect(test.authBlockingSignInV2TriggerCount).to.equal(1); + }); - const response = await test.writeToSpecificStorageBucket(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); + it("should sign in a user in the auth emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT * 2); + const response = await test.signInUserFromAuth(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - it("should have triggered cloud functions", () => { - /* on object create two events fire (finalize & metadata update) */ - // default bucket - expect(test.storageFinalizedTriggerCount).to.equal(0); - expect(test.storageMetadataTriggerCount).to.equal(0); - expect(test.storageV2FinalizedTriggerCount).to.equal(0); - expect(test.storageV2MetadataTriggerCount).to.equal(0); - expect(test.storageDeletedTriggerCount).to.equal(0); - expect(test.storageV2DeletedTriggerCount).to.equal(0); - // specific bucket - expect(test.storageBucketFinalizedTriggerCount).to.equal(1); - expect(test.storageBucketMetadataTriggerCount).to.equal(1); - expect(test.storageBucketV2FinalizedTriggerCount).to.equal(1); - expect(test.storageBucketV2MetadataTriggerCount).to.equal(1); - expect(test.storageBucketDeletedTriggerCount).to.equal(0); - expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); - test.resetCounts(); + it("should have triggered cloud functions", () => { + expect(test.authBlockingSignInV2TriggerCount).to.equal(2); + }); }); - it("should write, update, and delete from the default bucket of the storage emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); + describe("storage emulator triggered functions", () => { + it("should write to the default bucket of storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - const response = await test.updateDeleteFromDefaultStorage(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); - - it("should have triggered cloud functions", () => { - /* on update two events fire (finalize & metadata update) */ - /* on delete one event fires (delete) */ - // default bucket - expect(test.storageFinalizedTriggerCount).to.equal(1); - expect(test.storageMetadataTriggerCount).to.equal(1); - expect(test.storageV2FinalizedTriggerCount).to.equal(1); - expect(test.storageV2MetadataTriggerCount).to.equal(1); - expect(test.storageDeletedTriggerCount).to.equal(1); - expect(test.storageV2DeletedTriggerCount).to.equal(1); - // specific bucket - expect(test.storageBucketFinalizedTriggerCount).to.equal(0); - expect(test.storageBucketMetadataTriggerCount).to.equal(0); - expect(test.storageBucketV2FinalizedTriggerCount).to.equal(0); - expect(test.storageBucketV2MetadataTriggerCount).to.equal(0); - expect(test.storageBucketDeletedTriggerCount).to.equal(0); - expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); - test.resetCounts(); - }); + const response = await test.writeToDefaultStorage(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - it("should write, update, and delete from a specific bucket of the storage emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); + it("should have triggered cloud functions", () => { + /* on object create one event fires (finalize) */ + // default bucket + expect(test.storageFinalizedTriggerCount).to.equal(1); + expect(test.storageV2FinalizedTriggerCount).to.equal(1); + expect(test.storageMetadataTriggerCount).to.equal(0); + expect(test.storageV2MetadataTriggerCount).to.equal(0); + expect(test.storageDeletedTriggerCount).to.equal(0); + expect(test.storageV2DeletedTriggerCount).to.equal(0); + // specific bucket + expect(test.storageBucketFinalizedTriggerCount).to.equal(0); + expect(test.storageBucketV2FinalizedTriggerCount).to.equal(0); + expect(test.storageBucketMetadataTriggerCount).to.equal(0); + expect(test.storageBucketV2MetadataTriggerCount).to.equal(0); + expect(test.storageBucketDeletedTriggerCount).to.equal(0); + expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); + test.resetCounts(); + }); - const response = await test.updateDeleteFromSpecificStorageBucket(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); + it("should write to a specific bucket of storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - it("should have triggered cloud functions", () => { - /* on update two events fire (finalize & metadata update) */ - /* on delete one event fires (delete) */ - // default bucket - expect(test.storageFinalizedTriggerCount).to.equal(0); - expect(test.storageMetadataTriggerCount).to.equal(0); - expect(test.storageV2FinalizedTriggerCount).to.equal(0); - expect(test.storageV2MetadataTriggerCount).to.equal(0); - expect(test.storageDeletedTriggerCount).to.equal(0); - expect(test.storageV2DeletedTriggerCount).to.equal(0); - // specific bucket - expect(test.storageBucketFinalizedTriggerCount).to.equal(1); - expect(test.storageBucketMetadataTriggerCount).to.equal(1); - expect(test.storageBucketV2FinalizedTriggerCount).to.equal(1); - expect(test.storageBucketV2MetadataTriggerCount).to.equal(1); - expect(test.storageBucketDeletedTriggerCount).to.equal(1); - expect(test.storageBucketV2DeletedTriggerCount).to.equal(1); - test.resetCounts(); - }); -}); + const response = await test.writeToSpecificStorageBucket(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); -describe("import/export end to end", () => { - it("should be able to import/export firestore data", async function (this) { - this.timeout(2 * TEST_SETUP_TIMEOUT); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - // Start up emulator suite - const emulatorsCLI = new CLIProcess("1", __dirname); - await emulatorsCLI.start( - "emulators:start", - FIREBASE_PROJECT, - ["--only", "firestore"], - (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - } - ); + it("should have triggered cloud functions", () => { + /* on object create one event fires (finalize) */ + // default bucket + expect(test.storageFinalizedTriggerCount).to.equal(0); + expect(test.storageV2FinalizedTriggerCount).to.equal(0); + expect(test.storageMetadataTriggerCount).to.equal(0); + expect(test.storageV2MetadataTriggerCount).to.equal(0); + expect(test.storageDeletedTriggerCount).to.equal(0); + expect(test.storageV2DeletedTriggerCount).to.equal(0); + // specific bucket + expect(test.storageBucketFinalizedTriggerCount).to.equal(1); + expect(test.storageBucketV2FinalizedTriggerCount).to.equal(1); + expect(test.storageBucketMetadataTriggerCount).to.equal(0); + expect(test.storageBucketV2MetadataTriggerCount).to.equal(0); + expect(test.storageBucketDeletedTriggerCount).to.equal(0); + expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); + test.resetCounts(); + }); - // Ask for export - const exportCLI = new CLIProcess("2", __dirname); - const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); - await exportCLI.start("emulators:export", FIREBASE_PROJECT, [exportPath], (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes("Export complete"); - }); - await exportCLI.stop(); - - // Stop the suite - await emulatorsCLI.stop(); - - // Attempt to import - const importCLI = new CLIProcess("3", __dirname); - await importCLI.start( - "emulators:start", - FIREBASE_PROJECT, - ["--only", "firestore", "--import", exportPath], - (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - } - ); + it("should write and update metadata from the default bucket of the storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - await importCLI.stop(); + const response = await test.updateMetadataDefaultStorage(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - expect(true).to.be.true; - }); + it("should have triggered cloud functions", () => { + /* on object create one event fires (finalize) */ + /* on update one event fires (metadataUpdate) */ + // default bucket + expect(test.storageFinalizedTriggerCount).to.equal(1); + expect(test.storageV2FinalizedTriggerCount).to.equal(1); + expect(test.storageMetadataTriggerCount).to.equal(1); + expect(test.storageV2MetadataTriggerCount).to.equal(1); + expect(test.storageDeletedTriggerCount).to.equal(0); + expect(test.storageV2DeletedTriggerCount).to.equal(0); + // specific bucket + expect(test.storageBucketFinalizedTriggerCount).to.equal(0); + expect(test.storageBucketV2FinalizedTriggerCount).to.equal(0); + expect(test.storageBucketMetadataTriggerCount).to.equal(0); + expect(test.storageBucketV2MetadataTriggerCount).to.equal(0); + expect(test.storageBucketDeletedTriggerCount).to.equal(0); + expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); + test.resetCounts(); + }); - it("should be able to import/export rtdb data", async function (this) { - this.timeout(2 * TEST_SETUP_TIMEOUT); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - // Start up emulator suite - const emulatorsCLI = new CLIProcess("1", __dirname); - await emulatorsCLI.start( - "emulators:start", - FIREBASE_PROJECT, - ["--only", "database"], - (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - } - ); + it("should write and update metadata from a specific bucket of the storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - // Write some data to export - const config = readConfig(); - const port = config.emulators!.database.port; - const aApp = admin.initializeApp( - { - projectId: FIREBASE_PROJECT, - databaseURL: `http://localhost:${port}?ns=namespace-a`, - credential: ADMIN_CREDENTIAL, - }, - "rtdb-export-a" - ); - const bApp = admin.initializeApp( - { - projectId: FIREBASE_PROJECT, - databaseURL: `http://localhost:${port}?ns=namespace-b`, - credential: ADMIN_CREDENTIAL, - }, - "rtdb-export-b" - ); - const cApp = admin.initializeApp( - { - projectId: FIREBASE_PROJECT, - databaseURL: `http://localhost:${port}?ns=namespace-c`, - credential: ADMIN_CREDENTIAL, - }, - "rtdb-export-c" - ); + const response = await test.updateMetadataSpecificStorageBucket(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - // Write to two namespaces - const aRef = aApp.database().ref("ns"); - await aRef.set("namespace-a"); - const bRef = bApp.database().ref("ns"); - await bRef.set("namespace-b"); - - // Read from a third - const cRef = cApp.database().ref("ns"); - await cRef.once("value"); - - // Ask for export - const exportCLI = new CLIProcess("2", __dirname); - const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); - await exportCLI.start("emulators:export", FIREBASE_PROJECT, [exportPath], (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes("Export complete"); - }); - await exportCLI.stop(); - - // Check that the right export files are created - const dbExportPath = path.join(exportPath, "database_export"); - const dbExportFiles = fs.readdirSync(dbExportPath); - expect(dbExportFiles).to.eql(["namespace-a.json", "namespace-b.json"]); - - // Stop the suite - await emulatorsCLI.stop(); - - // Attempt to import - const importCLI = new CLIProcess("3", __dirname); - await importCLI.start( - "emulators:start", - FIREBASE_PROJECT, - ["--only", "database", "--import", exportPath, "--export-on-exit"], - (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - } - ); + it("should have triggered cloud functions", () => { + /* on object create one event fires (finalize) */ + /* on update one event fires (metadataUpdate) */ + // default bucket + expect(test.storageFinalizedTriggerCount).to.equal(0); + expect(test.storageV2FinalizedTriggerCount).to.equal(0); + expect(test.storageMetadataTriggerCount).to.equal(0); + expect(test.storageV2MetadataTriggerCount).to.equal(0); + expect(test.storageDeletedTriggerCount).to.equal(0); + expect(test.storageV2DeletedTriggerCount).to.equal(0); + // specific bucket + expect(test.storageBucketFinalizedTriggerCount).to.equal(1); + expect(test.storageBucketV2FinalizedTriggerCount).to.equal(1); + expect(test.storageBucketMetadataTriggerCount).to.equal(1); + expect(test.storageBucketV2MetadataTriggerCount).to.equal(1); + expect(test.storageBucketDeletedTriggerCount).to.equal(0); + expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); + test.resetCounts(); + }); - // Read the data - const aSnap = await aRef.once("value"); - const bSnap = await bRef.once("value"); - expect(aSnap.val()).to.eql("namespace-a"); - expect(bSnap.val()).to.eql("namespace-b"); + it("should write and delete from the default bucket of the storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - // Delete all of the import files - for (const f of fs.readdirSync(dbExportPath)) { - const fullPath = path.join(dbExportPath, f); - fs.unlinkSync(fullPath); - } + const response = await test.updateDeleteFromDefaultStorage(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - // Delete all the data in one namespace - await bApp.database().ref().set(null); + it("should have triggered cloud functions", () => { + /* on create one event fires (finalize) */ + /* on delete one event fires (delete) */ + // default bucket + expect(test.storageFinalizedTriggerCount).to.equal(1); + expect(test.storageV2FinalizedTriggerCount).to.equal(1); + expect(test.storageMetadataTriggerCount).to.equal(0); + expect(test.storageV2MetadataTriggerCount).to.equal(0); + expect(test.storageDeletedTriggerCount).to.equal(1); + expect(test.storageV2DeletedTriggerCount).to.equal(1); + // specific bucket + expect(test.storageBucketFinalizedTriggerCount).to.equal(0); + expect(test.storageBucketV2FinalizedTriggerCount).to.equal(0); + expect(test.storageBucketMetadataTriggerCount).to.equal(0); + expect(test.storageBucketV2MetadataTriggerCount).to.equal(0); + expect(test.storageBucketDeletedTriggerCount).to.equal(0); + expect(test.storageBucketV2DeletedTriggerCount).to.equal(0); + test.resetCounts(); + }); - // Stop the CLI (which will export on exit) - await importCLI.stop(); + it("should write and delete from a specific bucket of the storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - // Confirm the data exported is as expected - const aPath = path.join(dbExportPath, "namespace-a.json"); - const aData = JSON.parse(fs.readFileSync(aPath).toString()); - expect(aData).to.deep.equal({ ns: "namespace-a" }); + const response = await test.updateDeleteFromSpecificStorageBucket(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - const bPath = path.join(dbExportPath, "namespace-b.json"); - const bData = JSON.parse(fs.readFileSync(bPath).toString()); - expect(bData).to.equal(null); + it("should have triggered cloud functions", () => { + /* on create one event fires (finalize) */ + /* on delete one event fires (delete) */ + // default bucket + expect(test.storageFinalizedTriggerCount).to.equal(0); + expect(test.storageV2FinalizedTriggerCount).to.equal(0); + expect(test.storageMetadataTriggerCount).to.equal(0); + expect(test.storageV2MetadataTriggerCount).to.equal(0); + expect(test.storageDeletedTriggerCount).to.equal(0); + expect(test.storageV2DeletedTriggerCount).to.equal(0); + // specific bucket + expect(test.storageBucketFinalizedTriggerCount).to.equal(1); + expect(test.storageBucketV2FinalizedTriggerCount).to.equal(1); + expect(test.storageBucketMetadataTriggerCount).to.equal(0); + expect(test.storageBucketV2MetadataTriggerCount).to.equal(0); + expect(test.storageBucketDeletedTriggerCount).to.equal(1); + expect(test.storageBucketV2DeletedTriggerCount).to.equal(1); + test.resetCounts(); + }); }); - it("should be able to import/export auth data", async function (this) { - this.timeout(2 * TEST_SETUP_TIMEOUT); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - // Start up emulator suite - const project = FIREBASE_PROJECT || "example"; - const emulatorsCLI = new CLIProcess("1", __dirname); + describe("callable functions", () => { + it("should make a call to v1 callable function", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); + const response = await test.invokeCallableFunction("onCall", { data: "foobar" }); + expect(response.status).to.equal(200); + const body = await response.json(); + expect(body).to.deep.equal({ result: "foobar" }); }); - // Create some accounts to export: - const config = readConfig(); - const port = config.emulators!.auth.port; - try { - process.env.FIREBASE_AUTH_EMULATOR_HOST = `localhost:${port}`; - const adminApp = admin.initializeApp( - { - projectId: project, - credential: ADMIN_CREDENTIAL, - }, - "admin-app" - ); - await adminApp - .auth() - .createUser({ uid: "123", email: "foo@example.com", password: "testing" }); - await adminApp - .auth() - .createUser({ uid: "456", email: "bar@example.com", emailVerified: true }); - - // Ask for export - const exportCLI = new CLIProcess("2", __dirname); - const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); - await exportCLI.start("emulators:export", project, [exportPath], (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes("Export complete"); - }); - await exportCLI.stop(); - - // Stop the suite - await emulatorsCLI.stop(); - - // Confirm the data is exported as expected - const configPath = path.join(exportPath, "auth_export", "config.json"); - const configData = JSON.parse(fs.readFileSync(configPath).toString()); - expect(configData).to.deep.equal({ - signIn: { - allowDuplicateEmails: false, - }, - usageMode: "DEFAULT", - }); - - const accountsPath = path.join(exportPath, "auth_export", "accounts.json"); - const accountsData = JSON.parse(fs.readFileSync(accountsPath).toString()); - expect(accountsData.users).to.have.length(2); - expect(accountsData.users[0]).to.deep.contain({ - localId: "123", - email: "foo@example.com", - emailVerified: false, - providerUserInfo: [ - { - email: "foo@example.com", - federatedId: "foo@example.com", - providerId: "password", - rawId: "foo@example.com", - }, - ], - }); - expect(accountsData.users[0].passwordHash).to.match(/:password=testing$/); - expect(accountsData.users[1]).to.deep.contain({ - localId: "456", - email: "bar@example.com", - emailVerified: true, - }); - - // Attempt to import - const importCLI = new CLIProcess("3", __dirname); - await importCLI.start( - "emulators:start", - project, - ["--only", "auth", "--import", exportPath], - (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - } - ); - - // Check users are indeed imported correctly - const user1 = await adminApp.auth().getUserByEmail("foo@example.com"); - expect(user1.passwordHash).to.match(/:password=testing$/); - const user2 = await adminApp.auth().getUser("456"); - expect(user2.emailVerified).to.be.true; - - await importCLI.stop(); - } finally { - delete process.env.FIREBASE_AUTH_EMULATOR_HOST; - } - }); - - it("should be able to import/export auth data with many users", async function (this) { - this.timeout(2 * TEST_SETUP_TIMEOUT); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - // Start up emulator suite - const project = FIREBASE_PROJECT || "example"; - const emulatorsCLI = new CLIProcess("1", __dirname); + it("should make a call to v2 callable function", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => { - if (typeof data !== "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); + const response = await test.invokeCallableFunction("oncallv2", { data: "foobar" }); + expect(response.status).to.equal(200); + const body = await response.json(); + expect(body).to.deep.equal({ result: "foobar" }); }); - - // Create some accounts to export: - const accountCount = 777; // ~120KB data when exported - const config = readConfig(); - const port = config.emulators!.auth.port; - try { - process.env.FIREBASE_AUTH_EMULATOR_HOST = `localhost:${port}`; - const adminApp = admin.initializeApp( - { - projectId: project, - credential: ADMIN_CREDENTIAL, - }, - "admin-app2" - ); - for (let i = 0; i < accountCount; i++) { - await adminApp - .auth() - .createUser({ uid: `u${i}`, email: `u${i}@example.com`, password: "testing" }); - } - // Ask for export - const exportCLI = new CLIProcess("2", __dirname); - const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); - await exportCLI.start("emulators:export", project, [exportPath], (data: unknown) => { - if (typeof data !== "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes("Export complete"); - }); - await exportCLI.stop(); - - // Stop the suite - await emulatorsCLI.stop(); - - // Confirm the data is exported as expected - const configPath = path.join(exportPath, "auth_export", "config.json"); - const configData = JSON.parse(fs.readFileSync(configPath).toString()); - expect(configData).to.deep.equal({ - signIn: { - allowDuplicateEmails: false, - }, - usageMode: "DEFAULT", - }); - - const accountsPath = path.join(exportPath, "auth_export", "accounts.json"); - const accountsData = JSON.parse(fs.readFileSync(accountsPath).toString()); - expect(accountsData.users).to.have.length(accountCount); - - // Attempt to import - const importCLI = new CLIProcess("3", __dirname); - await importCLI.start( - "emulators:start", - project, - ["--only", "auth", "--import", exportPath], - (data: unknown) => { - if (typeof data !== "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - } - ); - - // Check users are indeed imported correctly - const user = await adminApp.auth().getUserByEmail(`u${accountCount - 1}@example.com`); - expect(user.passwordHash).to.match(/:password=testing$/); - - await importCLI.stop(); - } finally { - delete process.env.FIREBASE_AUTH_EMULATOR_HOST; - } }); - it("should be able to export / import auth data with no users", async function (this) { - this.timeout(2 * TEST_SETUP_TIMEOUT); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - // Start up emulator suite - const project = FIREBASE_PROJECT || "example"; - const emulatorsCLI = new CLIProcess("1", __dirname); - - await emulatorsCLI.start("emulators:start", project, ["--only", "auth"], (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - }); - - // Ask for export (with no users) - const exportCLI = new CLIProcess("2", __dirname); - const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); - await exportCLI.start("emulators:export", project, [exportPath], (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes("Export complete"); - }); - await exportCLI.stop(); - - // Stop the suite - await emulatorsCLI.stop(); - - // Confirm the data is exported as expected - const configPath = path.join(exportPath, "auth_export", "config.json"); - const configData = JSON.parse(fs.readFileSync(configPath).toString()); - expect(configData).to.deep.equal({ - signIn: { - allowDuplicateEmails: false, - }, - usageMode: "DEFAULT", - }); - - const accountsPath = path.join(exportPath, "auth_export", "accounts.json"); - const accountsData = JSON.parse(fs.readFileSync(accountsPath).toString()); - expect(accountsData.users).to.have.length(0); - - // Attempt to import - const importCLI = new CLIProcess("3", __dirname); - await importCLI.start( - "emulators:start", - project, - ["--only", "auth", "--import", exportPath], - (data: unknown) => { - if (typeof data != "string" && !Buffer.isBuffer(data)) { - throw new Error(`data is not a string or buffer (${typeof data})`); - } - return data.includes(ALL_EMULATORS_STARTED_LOG); - } - ); - await importCLI.stop(); + it("should enforce timeout", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + const v2response = await test.invokeHttpFunction("onreqv2timeout"); + expect(v2response.status).to.equal(500); }); - it("should be able to import/export storage data", async function (this) { - this.timeout(2 * TEST_SETUP_TIMEOUT); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - // Start up emulator suite - const emulatorsCLI = new CLIProcess("1", __dirname); - await emulatorsCLI.start( - "emulators:start", - FIREBASE_PROJECT, - ["--only", "storage"], - logIncludes(ALL_EMULATORS_STARTED_LOG) - ); - - const credPath = path.join(__dirname, "service-account-key.json"); - const credential = fs.existsSync(credPath) - ? admin.credential.cert(credPath) - : admin.credential.applicationDefault(); - - const config = readConfig(); - const port = config.emulators!.storage.port; - process.env.STORAGE_EMULATOR_HOST = `http://localhost:${port}`; - - // Write some data to export - const aApp = admin.initializeApp( - { - projectId: FIREBASE_PROJECT, - storageBucket: "bucket-a", - credential, - }, - "storage-export-a" - ); - const bApp = admin.initializeApp( - { - projectId: FIREBASE_PROJECT, - storageBucket: "bucket-b", - credential, - }, - "storage-export-b" - ); - - // Write data to two buckets - await aApp.storage().bucket().file("a/b.txt").save("a/b hello, world!"); - await aApp.storage().bucket().file("c/d.txt").save("c/d hello, world!"); - await bApp.storage().bucket().file("e/f.txt").save("e/f hello, world!"); - await bApp.storage().bucket().file("g/h.txt").save("g/h hello, world!"); - - // Ask for export - const exportCLI = new CLIProcess("2", __dirname); - const exportPath = fs.mkdtempSync(path.join(os.tmpdir(), "emulator-data")); - await exportCLI.start( - "emulators:export", - FIREBASE_PROJECT, - [exportPath], - logIncludes("Export complete") - ); - await exportCLI.stop(); - - // Check that the right export files are created - const storageExportPath = path.join(exportPath, "storage_export"); - const storageExportFiles = fs.readdirSync(storageExportPath).sort(); - expect(storageExportFiles).to.eql(["blobs", "buckets.json", "metadata"]); - - // Stop the suite - await emulatorsCLI.stop(); - - // Attempt to import - const importCLI = new CLIProcess("3", __dirname); - await importCLI.start( - "emulators:start", - FIREBASE_PROJECT, - ["--only", "storage", "--import", exportPath], - logIncludes(ALL_EMULATORS_STARTED_LOG) - ); - - // List the files - const [aFiles] = await aApp.storage().bucket().getFiles({ - prefix: "a/", + describe("disable/enableBackgroundTriggers", () => { + before(() => { + test.resetCounts(); }); - const aFileNames = aFiles.map((f) => f.name).sort(); - expect(aFileNames).to.eql(["a/b.txt"]); - const [bFiles] = await bApp.storage().bucket().getFiles({ - prefix: "e/", + it("should disable background triggers", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + const response = await test.disableBackgroundTriggers(); + expect(response.status).to.equal(200); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + + await Promise.all([ + // TODO(danielylee): Trying to respond to all triggers at once often results in Functions + // Emulator hanging indefinitely. Only triggering 1 trigger for now. Re-enable other triggers + // once the root cause is identified. + // test.writeToRtdb(), + // test.writeToFirestore(), + // test.writeToPubsub(), + // test.writeToDefaultStorage(), + test.writeToAuth(), + ]); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS * 2)); + + // expect(test.rtdbTriggerCount).to.equal(0); + // expect(test.rtdbV2TriggerCount).to.eq(0); + // expect(test.firestoreTriggerCount).to.equal(0); + // expect(test.pubsubTriggerCount).to.equal(0); + // expect(test.pubsubV2TriggerCount).to.equal(0); + expect(test.authTriggerCount).to.equal(0); }); - const bFileNames = bFiles.map((f) => f.name).sort(); - expect(bFileNames).to.eql(["e/f.txt"]); - // TODO: this operation fails due to a bug in the Storage emulator - // https://github.com/firebase/firebase-tools/pull/3320 - // - // Read a file and check content - // const [f] = await aApp.storage().bucket().file("a/b.txt").get(); - // const [buf] = await f.download(); - // expect(buf.toString()).to.eql("a/b hello, world!"); + it("should re-enable background triggers", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); + + const response = await test.enableBackgroundTriggers(); + expect(response.status).to.equal(200); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + + await Promise.all([ + // TODO(danielylee): Trying to respond to all triggers at once often results in Functions + // Emulator hanging indefinitely. Only triggering 1 trigger for now. Re-enable other triggers + // once the root cause is identified. + // test.writeToRtdb(), + // test.writeToFirestore(), + // test.writeToPubsub(), + // test.writeToDefaultStorage(), + test.writeToAuth(), + ]); + + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS * 3)); + // TODO(danielylee): Trying to respond to all triggers at once often results in Functions + // Emulator hanging indefinitely. Only triggering 1 trigger for now. Re-enable other triggers + // once the root cause is identified. + // expect(test.rtdbTriggerCount).to.equal(1); + // expect(test.rtdbV2TriggerCount).to.eq(1); + // expect(test.firestoreTriggerCount).to.equal(1); + // expect(test.pubsubTriggerCount).to.equal(1); + // expect(test.pubsubV2TriggerCount).to.equal(1); + expect(test.authTriggerCount).to.equal(1); + }); }); }); diff --git a/scripts/triggers-end-to-end-tests/triggers/.gitignore b/scripts/triggers-end-to-end-tests/triggers/.gitignore new file mode 100644 index 00000000000..884afa60ceb --- /dev/null +++ b/scripts/triggers-end-to-end-tests/triggers/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.eslintrc +package-lock.json diff --git a/scripts/triggers-end-to-end-tests/triggers/index.js b/scripts/triggers-end-to-end-tests/triggers/index.js new file mode 100644 index 00000000000..7936ecb56ee --- /dev/null +++ b/scripts/triggers-end-to-end-tests/triggers/index.js @@ -0,0 +1,160 @@ +const admin = require("firebase-admin"); +const functions = require("firebase-functions"); +const { PubSub } = require("@google-cloud/pubsub"); +const { initializeApp } = require("firebase/app"); +const { + getAuth, + connectAuthEmulator, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, +} = require("firebase/auth"); + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; + +/* + * We install onWrite triggers for START_DOCUMENT_NAME in both the firestore and + * database emulators. From each respective onWrite trigger, we write a document + * to both the firestore and database emulators. This exercises the + * bidirectional communication between cloud functions and each emulator. + */ +const START_DOCUMENT_NAME = "test/start"; + +const PUBSUB_TOPIC = "test-topic"; +const PUBSUB_SCHEDULED_TOPIC = "firebase-schedule-pubsubScheduled"; + +const STORAGE_FILE_NAME = "test-file.txt"; + +const pubsub = new PubSub(); + +// init the Firebase Admin SDK +admin.initializeApp(); + +// init the Firebase JS SDK +const app = initializeApp( + { + apiKey: "fake-api-key", + projectId: `${FIREBASE_PROJECT}`, + authDomain: `${FIREBASE_PROJECT}.firebaseapp.com`, + storageBucket: `${FIREBASE_PROJECT}.appspot.com`, + appId: "fake-app-id", + }, + "TRIGGERS_END_TO_END", +); +const auth = getAuth(app); +connectAuthEmulator(auth, `http://${process.env.FIREBASE_AUTH_EMULATOR_HOST}`); + +exports.deleteFromFirestore = functions.https.onRequest(async (req, res) => { + await admin.firestore().doc(START_DOCUMENT_NAME).delete(); + res.json({ deleted: true }); +}); + +exports.deleteFromRtdb = functions.https.onRequest(async (req, res) => { + await admin.database().ref(START_DOCUMENT_NAME).remove(); + res.json({ deleted: true }); +}); + +exports.writeToFirestore = functions.https.onRequest(async (req, res) => { + const ref = admin.firestore().doc(START_DOCUMENT_NAME); + await ref.set({ start: new Date().toISOString() }); + ref.get().then((snap) => { + res.json({ data: snap.data() }); + }); +}); + +exports.writeToRtdb = functions.https.onRequest(async (req, res) => { + const ref = admin.database().ref(START_DOCUMENT_NAME); + await ref.set({ start: new Date().toISOString() }); + ref.once("value", (snap) => { + res.json({ data: snap }); + }); +}); + +exports.writeToPubsub = functions.https.onRequest(async (req, res) => { + const msg = await pubsub.topic(PUBSUB_TOPIC).publishJSON({ foo: "bar" }, { attr: "val" }); + console.log("PubSub Emulator Host", process.env.PUBSUB_EMULATOR_HOST); + console.log("Wrote PubSub Message", msg); + res.json({ published: "ok" }); +}); + +exports.writeToScheduledPubsub = functions.https.onRequest(async (req, res) => { + const msg = await pubsub + .topic(PUBSUB_SCHEDULED_TOPIC) + .publishJSON({ foo: "bar" }, { attr: "val" }); + console.log("PubSub Emulator Host", process.env.PUBSUB_EMULATOR_HOST); + console.log("Wrote Scheduled PubSub Message", msg); + res.json({ published: "ok" }); +}); + +exports.writeToAuth = functions.https.onRequest(async (req, res) => { + const time = new Date().getTime(); + await admin.auth().createUser({ + uid: `uid${time}`, + email: `user${time}@example.com`, + }); + + res.json({ created: "ok" }); +}); + +exports.createUserFromAuth = functions.https.onRequest(async (req, res) => { + await createUserWithEmailAndPassword(auth, "email@gmail.com", "mypassword"); + + res.json({ created: "ok" }); +}); + +exports.signInUserFromAuth = functions.https.onRequest(async (req, res) => { + await signInWithEmailAndPassword(auth, "email@gmail.com", "mypassword"); + + res.json({ done: "ok" }); +}); + +exports.writeToDefaultStorage = functions.https.onRequest(async (req, res) => { + await admin.storage().bucket().file(STORAGE_FILE_NAME).save("hello world!"); + console.log("Wrote to default Storage bucket"); + res.json({ created: "ok" }); +}); + +exports.writeToSpecificStorageBucket = functions.https.onRequest(async (req, res) => { + await admin.storage().bucket("test-bucket").file(STORAGE_FILE_NAME).save("hello world!"); + console.log("Wrote to a specific Storage bucket"); + res.json({ created: "ok" }); +}); + +exports.updateMetadataFromDefaultStorage = functions.https.onRequest(async (req, res) => { + await admin.storage().bucket().file(STORAGE_FILE_NAME).save("hello metadata update!"); + console.log("Wrote to Storage bucket"); + await admin.storage().bucket().file(STORAGE_FILE_NAME).setMetadata({ somekey: "someval" }); + console.log("Updated metadata of default Storage bucket"); + res.json({ done: "ok" }); +}); + +exports.updateMetadataFromSpecificStorageBucket = functions.https.onRequest(async (req, res) => { + await admin + .storage() + .bucket("test-bucket") + .file(STORAGE_FILE_NAME) + .save("hello metadata update!"); + console.log("Wrote to a specific Storage bucket"); + await admin + .storage() + .bucket("test-bucket") + .file(STORAGE_FILE_NAME) + .setMetadata({ somenewkey: "somenewval" }); + console.log("Updated metadata of a specific Storage bucket"); + res.json({ done: "ok" }); +}); + +exports.updateDeleteFromDefaultStorage = functions.https.onRequest(async (req, res) => { + await admin.storage().bucket().file(STORAGE_FILE_NAME).save("something new!"); + console.log("Wrote to Storage bucket"); + await admin.storage().bucket().file(STORAGE_FILE_NAME).delete(); + console.log("Deleted from Storage bucket"); + res.json({ done: "ok" }); +}); + +exports.updateDeleteFromSpecificStorageBucket = functions.https.onRequest(async (req, res) => { + await admin.storage().bucket("test-bucket").file(STORAGE_FILE_NAME).save("something new!"); + console.log("Wrote to a specific Storage bucket"); + await admin.storage().bucket("test-bucket").file(STORAGE_FILE_NAME).delete(); + console.log("Deleted from a specific Storage bucket"); + res.json({ done: "ok" }); +}); diff --git a/scripts/triggers-end-to-end-tests/triggers/package-lock.json b/scripts/triggers-end-to-end-tests/triggers/package-lock.json new file mode 100644 index 00000000000..d1b8e1c7b82 --- /dev/null +++ b/scripts/triggers-end-to-end-tests/triggers/package-lock.json @@ -0,0 +1,6661 @@ +{ + "name": "functions", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "@firebase/database-compat": "0.1.2", + "@google-cloud/pubsub": "^3.0.1", + "firebase": "^9.9.0", + "firebase-admin": "^11.0.0", + "firebase-functions": "^3.24.1" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.8.0.tgz", + "integrity": "sha512-wkcwainNm8Cu2xkJpDSHfhBSdDJn86Q1TZNmLWc67VrhZUHXIKXxIqb65/tNUVE+I8+sFiDDNwA+9R3MqTQTaA==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.1.13.tgz", + "integrity": "sha512-QC1DH/Dwc8fBihn0H+jocBWyE17GF1fOCpCrpAiQ2u16F/NqsVDVG4LjIqdhq963DXaXneNY7oDwa25Up682AA==", + "dependencies": { + "@firebase/analytics": "0.8.0", + "@firebase/analytics-types": "0.7.0", + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.7.0.tgz", + "integrity": "sha512-DNE2Waiwy5+zZnCfintkDtBfaW6MjIG883474v6Z0K1XZIvl76cLND4iv0YUb48leyF+PJK1KO2XrgHb/KpmhQ==" + }, + "node_modules/@firebase/app": { + "version": "0.7.28", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.28.tgz", + "integrity": "sha512-Ti0AZSDy3F5uH0Mer3dstnxGqyjaDo52E40ZRjYgxYlJXlo+LdVF8AI4OE7ZgSz6h0yPODvT2me8/ytVFSys2A==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.5.11.tgz", + "integrity": "sha512-v+Ubf5ZDU79Pkr2q3bspBzv+NmJ3se9+2QJt37cRg00yvdjcb+RAHCLdP2abPEmOj5tZoibbm9IHxsiw1WIxLg==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.2.11.tgz", + "integrity": "sha512-AUG1MxbbXHjRg5o3I8jK+3HRvm/CmFbMpswp0eD8Yf1EULPagn3uGArYeDQmrbD4Hvv0lsngweTSLB9BbYp6Jg==", + "dependencies": { + "@firebase/app-check": "0.5.11", + "@firebase/app-check-types": "0.4.0", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.1.0.tgz", + "integrity": "sha512-uZfn9s4uuRsaX5Lwx+gFP3B6YsyOKUE+Rqa6z9ojT4VSRAsZFko9FRn6OxQUA1z5t5d08fY4pf+/+Dkd5wbdbA==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.4.0.tgz", + "integrity": "sha512-SsWafqMABIOu7zLgWbmwvHGOeQQVQlwm42kwwubsmfLmL4Sf5uGpBfDhQ0CAkpi7bkJ/NwNFKafNDL9prRNP0Q==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.29.tgz", + "integrity": "sha512-plkKiG6sGRfh1APWSfF7FeDF79zB2kQ/Y1M1Vy7IDT6rvZhK0+ol0j7Uad2t3cpd4j615dkLIKyiG4A7RojKuw==", + "dependencies": { + "@firebase/app": "0.7.28", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + }, + "node_modules/@firebase/auth": { + "version": "0.20.5", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.20.5.tgz", + "integrity": "sha512-SbKj7PCAuL0lXEToUOoprc1im2Lr/bzOePXyPC7WWqVgdVBt0qovbfejlzKYwJLHUAPg9UW1y3XYe3IlbXr77w==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "selenium-webdriver": "4.1.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.18.tgz", + "integrity": "sha512-Fw2PJS0G/tGrfyEBcYJQ42sfy5+sANrK5xd7tuzgV7zLFW5rYkHUIZngXjuOBwLOcfO2ixa/FavfeJle3oJ38Q==", + "dependencies": { + "@firebase/auth": "0.20.5", + "@firebase/auth-types": "0.11.0", + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "selenium-webdriver": "4.1.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth-types": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.11.0.tgz", + "integrity": "sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.17.tgz", + "integrity": "sha512-mTM5CBSIlmI+i76qU4+DhuExnWtzcPS3cVgObA3VAjliPPr3GrUlTaaa8KBGfxsD27juQxMsYA0TvCR5X+GQ3Q==", + "dependencies": { + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.2.tgz", + "integrity": "sha512-Y1LZR1LIQM8YKMkeUPpAq3/e53hcfcXO+JEZ6vCzBeD6xRawqmpw6B5/DzePdCNNvjcqheXzSaR7T39eRZo/wA==", + "dependencies": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.7", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.2.tgz", + "integrity": "sha512-sV32QIRSNIBj/6OYtpmPzA/SfQz1/NBZbhxg9dIhGaSt9e5HaMxXRuz2lImudX0Sd/v8DKdExrxa++K6rKrRtA==", + "dependencies": { + "@firebase/component": "0.5.7", + "@firebase/database": "0.12.2", + "@firebase/database-types": "0.9.1", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "dependencies": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.1.tgz", + "integrity": "sha512-RUixK/YrbpxbfdE+nYP0wMcEsz1xPTnafP0q3UlSS/+fW744OITKtR1J0cMRaXbvY7EH0wUVTNVkrtgxYY8IgQ==", + "dependencies": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.4.0" + } + }, + "node_modules/@firebase/database-types/node_modules/@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database/node_modules/@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "dependencies": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database/node_modules/@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database/node_modules/@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.4.12.tgz", + "integrity": "sha512-EILCg3GFImeRd82fMq+sHMaEAW1PRdzzkEcVcG0B5rNokyTbGE/8xLs5Q+2mIZhiWEmE6U4yNmvXcBwarTtdgA==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "@firebase/webchannel-wrapper": "0.6.2", + "@grpc/grpc-js": "^1.3.2", + "@grpc/proto-loader": "^0.6.0", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.1.21.tgz", + "integrity": "sha512-Zb+HkjG+xE2ubVmJNN2zi7aE3hFKzfqdhg0rQZGOuPn7pOaJqsKHFlGESLJ5R/TRh7I6GfK6Oniwbimjy5ILbg==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/firestore": "3.4.12", + "@firebase/firestore-types": "2.5.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.0.tgz", + "integrity": "sha512-I6c2m1zUhZ5SH0cWPmINabDyH5w0PPFHk2UHsjBpKdZllzJZ2TwTkXbDtpHUZNmnc/zAa0WNMNMvcvbb/xJLKA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.8.4.tgz", + "integrity": "sha512-o1bB0xMyQKe+b246zGnjwHj4R6BH4mU2ZrSaa/3QvTpahUQ3hqYfkZPLOXCU7+vEFxHb3Hd4UUjkFhxoAcPqLA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.1.0", + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.17", + "@firebase/messaging-interop-types": "0.1.0", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.2.4.tgz", + "integrity": "sha512-Crfn6il1yXGuXkjSd8nKrqR4XxPvuP19g64bXpM6Ix67qOkQg676kyOuww0FF17xN0NSXHfG8Pyf+CUrx8wJ5g==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/functions": "0.8.4", + "@firebase/functions-types": "0.5.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.5.0.tgz", + "integrity": "sha512-qza0M5EwX+Ocrl1cYI14zoipUX4gI/Shwqv0C1nB864INAD42Dgv4v94BCyxGHBg2kzlWy8PNafdP7zPO8aJQA==" + }, + "node_modules/@firebase/installations": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.5.12.tgz", + "integrity": "sha512-Zq43fCE0PB5tGJ3ojzx5RNQzKdej1188qgAk22rwjuhP7npaG/PlJqDG1/V0ZjTLRePZ1xGrfXSPlA17c/vtNw==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.1.12.tgz", + "integrity": "sha512-BIhFpWIn/GkuOa+jnXkp3SDJT2RLYJF6MWpinHIBKFJs7MfrgYZ3zQ1AlhobDEql+bkD1dK4dB5sNcET2T+EyA==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/installations-types": "0.4.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.4.0.tgz", + "integrity": "sha512-nXxWKQDvBGctuvsizbUEJKfxXU9WAaDhon+j0jpjIfOJkvkj3YHqlLB/HeYjpUn85Pb22BjplpTnDn4Gm9pc3A==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.3.tgz", + "integrity": "sha512-POTJl07jOKTOevLXrTvJD/VZ0M6PnJXflbAh5J9VGkmtXPXNG6MdZ9fmRgqYhXKTaDId6AQenQ262uwgpdtO0Q==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.9.16.tgz", + "integrity": "sha512-Yl9gGrAvJF6C1gg3+Cr2HxlL6APsDEkrorkFafmSP1l+rg1epZKoOAcKJbSF02Vtb50wfb9FqGGy8tzodgETxg==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/messaging-interop-types": "0.1.0", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.1.16.tgz", + "integrity": "sha512-uG7rWcXJzU8vvlEBFpwG1ndw/GURrrmKcwsHopEWbsPGjMRaVWa7XrdKbvIR7IZohqPzcC/V9L8EeqF4Q4lz8w==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/messaging": "0.9.16", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.1.0.tgz", + "integrity": "sha512-DbvUl/rXAZpQeKBnwz0NYY5OCqr2nFA0Bj28Fmr3NXGqR4PAkfTOHuQlVtLO1Nudo3q0HxAYLa68ZDAcuv2uKQ==" + }, + "node_modules/@firebase/performance": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.5.12.tgz", + "integrity": "sha512-MPVTkOkGrm2SMQgI1FPNBm85y2pPqlPb6VDjIMCWkVpAr6G1IZzUT24yEMySRcIlK/Hh7/Qu1Nu5ASRzRuX6+Q==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.1.12.tgz", + "integrity": "sha512-IBORzUeGY1MGdZnsix9Mu5z4+C3WHIwalu0usxvygL0EZKHztGG8bppYPGH/b5vvg8QyHs9U+Pn1Ot2jZhffQQ==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/performance": "0.5.12", + "@firebase/performance-types": "0.1.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.1.0.tgz", + "integrity": "sha512-6p1HxrH0mpx+622Ql6fcxFxfkYSBpE3LSuwM7iTtYU2nw91Hj6THC8Bc8z4nboIq7WvgsT/kOTYVVZzCSlXl8w==" + }, + "node_modules/@firebase/polyfill": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@firebase/polyfill/-/polyfill-0.3.36.tgz", + "integrity": "sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg==", + "dependencies": { + "core-js": "3.6.5", + "promise-polyfill": "8.1.3", + "whatwg-fetch": "2.0.4" + } + }, + "node_modules/@firebase/remote-config": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.3.11.tgz", + "integrity": "sha512-qA84dstrvVpO7rWT/sb2CLv1kjHVmz59SRFPKohJJYFBcPOGK4Pe4FWWhKAE9yg1Gnl0qYAGkahOwNawq3vE0g==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.1.12.tgz", + "integrity": "sha512-Yz7Gtb2rLa7ykXZX9DnSTId8CXd++jFFLW3foUImrYwJEtWgLJc7gwkRfd1M73IlKGNuQAY+DpUNF0n1dLbecA==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/remote-config": "0.3.11", + "@firebase/remote-config-types": "0.2.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.2.0.tgz", + "integrity": "sha512-hqK5sCPeZvcHQ1D6VjJZdW6EexLTXNMJfPdTwbD8NrXUw6UjWC4KWhLK/TSlL0QPsQtcKRkaaoP+9QCgKfMFPw==" + }, + "node_modules/@firebase/storage": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.9.9.tgz", + "integrity": "sha512-Zch7srLT2SIh9y2nCVv/4Kne0HULn7OPkmreY70BJTUJ+g5WLRjggBq6x9fV5ls9V38iqMWfn4prxzX8yIc08A==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.1.17.tgz", + "integrity": "sha512-nOYmnpI0gwoz5nROseMi9WbmHGf+xumfsOvdPyMZAjy0VqbDnpKIwmTUZQBdR+bLuB5oIkHQsvw9nbb1SH+PzQ==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/storage": "0.9.9", + "@firebase/storage-types": "0.6.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.6.0.tgz", + "integrity": "sha512-1LpWhcCb1ftpkP/akhzjzeFxgVefs6eMD2QeKiJJUGH1qOiows2w5o0sKCUSQrvrRQS1lz3SFGvNR1Ck/gqxeA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.6.3.tgz", + "integrity": "sha512-FujteO6Zjv6v8A4HS+t7c+PjU0Kaxj+rOnka0BsI/twUaCC9t8EQPmXpWZdk7XfszfahJn2pqsflUWUhtUkRlg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.2.tgz", + "integrity": "sha512-zThUKcqIU6utWzM93uEvhlh8qj8A5LMPFJPvk/ODb+8GSSif19xM2Lw1M2ijyBy8+6skSkQBbavPzOU5Oh/8tQ==" + }, + "node_modules/@google-cloud/firestore": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.1.tgz", + "integrity": "sha512-5q4sl1XCL8NH2y82KZ4WQGHDOPnrSMYq3JpIeKD5C0OCSb4MfckOTB9LeAQ3p5tlL+7UsVRHj0SyzKz27XZJjw==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.1", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/firestore/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + }, + "node_modules/@google-cloud/firestore/node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "optional": true, + "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/@google-cloud/paginator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.0.tgz", + "integrity": "sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ==", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/precise-date": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.4.tgz", + "integrity": "sha512-nOB+mZdevI/1Si0QAfxWfzzIqFdc7wrO+DYePFvgbOoMtvX+XfFTINNt7e9Zg66AbDbWCPRnikU+6f5LTm9Wyg==", + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/pubsub": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-3.0.1.tgz", + "integrity": "sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA==", + "dependencies": { + "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^8.0.2", + "google-gax": "^3.0.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "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/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.1.0.tgz", + "integrity": "sha512-hf+3bwuBwtXsugA2ULBc95qxrOqP2pOekLz34BJhcAKawt94vfeNyUKpYc0lZQ/3sCP6LqRa7UAdHA7i5UODzQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.4.0.tgz", + "integrity": "sha512-Hzl8soGpmyzja9w3kiFFcYJ7n5HNETpplY6cb67KR4QPlxp4FTTresO06qXHgHDhyIInmbLJXuwARjjpsKYGuQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "engines": { + "node": ">=10.13.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==" + }, + "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==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "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==", + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "node_modules/@types/duplexify": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.1.tgz", + "integrity": "sha512-n0zoEj/fMdMOvqbHxmqnza/kXyoGgJmEpsXjpP+gEqE1Ye4yNqc7xWipKnUoMpWhMuzJQSfK2gMrwlElly7OGQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.29", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", + "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, + "node_modules/@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "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==" + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + }, + "node_modules/@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "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==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "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==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "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" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "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==" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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==" + }, + "node_modules/core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "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==", + "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==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "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==" + }, + "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==" + }, + "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==", + "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==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "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==", + "optional": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.4.tgz", + "integrity": "sha512-x6lDDm/tBAzX9kmsPcZsNbvDs3Zey3+scsxaZElS8xWLgUMAg/oFLeewfUz0mu1CblHhhsu15jGkraldkFh8KQ==" + }, + "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==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.9.0.tgz", + "integrity": "sha512-EEUUEDcOTMlNMg4blQQrTmt9axWVI5GNL6XYgrK8kKZsgi8BAUP88b2AWW+UIb684Z/yNENMcRWaghOjOV4rSg==", + "dependencies": { + "@firebase/analytics": "0.8.0", + "@firebase/analytics-compat": "0.1.13", + "@firebase/app": "0.7.28", + "@firebase/app-check": "0.5.11", + "@firebase/app-check-compat": "0.2.11", + "@firebase/app-compat": "0.1.29", + "@firebase/app-types": "0.7.0", + "@firebase/auth": "0.20.5", + "@firebase/auth-compat": "0.2.18", + "@firebase/database": "0.13.3", + "@firebase/database-compat": "0.2.3", + "@firebase/firestore": "3.4.12", + "@firebase/firestore-compat": "0.1.21", + "@firebase/functions": "0.8.4", + "@firebase/functions-compat": "0.2.4", + "@firebase/installations": "0.5.12", + "@firebase/installations-compat": "0.1.12", + "@firebase/messaging": "0.9.16", + "@firebase/messaging-compat": "0.1.16", + "@firebase/performance": "0.5.12", + "@firebase/performance-compat": "0.1.12", + "@firebase/polyfill": "0.3.36", + "@firebase/remote-config": "0.3.11", + "@firebase/remote-config-compat": "0.1.12", + "@firebase/storage": "0.9.9", + "@firebase/storage-compat": "0.1.17", + "@firebase/util": "1.6.3" + } + }, + "node_modules/firebase-admin": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.4.1.tgz", + "integrity": "sha512-t5+Pf8rC01TW1KPD5U8Q45AEn7eK+FJaHlpzYStFb62J+MQmN/kB/PWUEsNn+7MNAQ0DZxFUCgJoi+bRmf83oQ==", + "dependencies": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.6", + "@firebase/database-types": "^0.9.13", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^2.1.4", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==" + }, + "node_modules/firebase-admin/node_modules/@firebase/auth-interop-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/component": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", + "dependencies": { + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", + "dependencies": { + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", + "dependencies": { + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database-types": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", + "dependencies": { + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/logger": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/util": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-functions": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "lodash": "^4.17.14", + "node-fetch": "^2.6.7" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.2.3.tgz", + "integrity": "sha512-zYX0QTm53wCazuej7O0xqbHl90r/v1PTXt/hwa0jo1YF8nDM+iBKnLDlkIoW66MDd0R6aGg4BvKzTTdJpvigUA==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "firebase-admin": ">=6.0.0", + "firebase-functions": ">=2.0.0" + } + }, + "node_modules/firebase/node_modules/@firebase/database": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.3.tgz", + "integrity": "sha512-ZE+QJqQUaCTZiIzGq3RJLo64HRMtbdaEwyDhfZyPEzMJV4kyLsw3cHdEHVCtBmdasTvwtpO2YRFmd4AXAoKtNw==", + "dependencies": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase/node_modules/@firebase/database-compat": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.3.tgz", + "integrity": "sha512-uwSMnbjlSQM5gQRq8OoBLs7uc7obwsl0D6kSDAnMOlPtPl9ert79Rq9faU/COjybsJ8l7tNXMVYYJo3mQ5XNrA==", + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/database": "0.13.3", + "@firebase/database-types": "0.9.11", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase/node_modules/@firebase/database-types": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.11.tgz", + "integrity": "sha512-27V3eFomWCZqLR6qb3Q9eS2lsUtulhSHeDNaL6fImwnhvMYTmf6ZwMfRWupgi8AFwW4s91g9Oc1/fkQtJGHKQw==", + "dependencies": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.6.3" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "node_modules/gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.1.0.tgz", + "integrity": "sha512-J/fNXEnqLgbr3kmeUshZCtHQia6ZiNbbrebVzpt/+LTeY6Ka9CtbQvloTjVGVO7nyYbs0KYeuIwgUC/t2Gp1Jw==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-gax/node_modules/proto3-json-serializer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.0.2.tgz", + "integrity": "sha512-wHxf8jYZ/LUP3M7XmULDKnbxBn+Bvk6SM+tDCPVTp9vraIzUi9hHsOBb1n2Y0VV0ukx4zBN/2vzMQYs4KWwRpg==", + "dependencies": { + "protobufjs": "^6.11.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-gax/node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "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/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/google-gax/node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "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/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/google-gax/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + }, + "node_modules/google-p12-pem": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.0.tgz", + "integrity": "sha512-lRTMn5ElBdDixv4a86bixejPSRk1boRtUowNepeKEVvYiFlkLuAJUVpEz6PfObDHYEKnZWq/9a2zC98xu62A9w==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/gtoken": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.0.tgz", + "integrity": "sha512-WPZcFw34wh2LUvbCUWI70GDhOlO7qHpSvFHFqq7d3Wvsf8dIJedE0lnUdOmsKuC0NgflKmF0LxIF38vsGeHHiQ==", + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gtoken/node_modules/gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==" + }, + "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==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "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==", + "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==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "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==", + "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==", + "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==" + }, + "node_modules/jszip": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz", + "integrity": "sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz", + "integrity": "sha512-mpArfgPkUpX11lNtGxsF/szkasUcbWHGplZl/uFvFO2NuMHmt0dQXIihh0rkPU2yQd5niQtuUHbXnG/WKiXF6Q==", + "dependencies": { + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^8.5.8", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=10 < 13 || >=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "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/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==" + }, + "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==" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "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==", + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "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==", + "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==", + "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==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "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==", + "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==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "engines": { + "node": ">=8" + } + }, + "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==", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise-polyfill": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz", + "integrity": "sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==" + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "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/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.1.tgz", + "integrity": "sha512-lxFKrlBt0OZzCWh/V0uPEN0vlr3OhdeXnpeY5OES+ckslm791Cb1D5P7lJUSnY7J5hiCjcyaUGmzCnIGDCUBig==", + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/selenium-webdriver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz", + "integrity": "sha512-e4Ap8vQvhipgBB8Ry9zBiKGkU6kHKyNnWiavGGLKkrdW81Zv7NVMtFOL/j3yX0G8QScM7XIXijKssNd4EUxSOw==", + "dependencies": { + "jszip": "^3.6.0", + "tmp": "^0.2.1", + "ws": ">=7.4.6" + }, + "engines": { + "node": ">= 10.15.0" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "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==", + "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==", + "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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "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==", + "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==" + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "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==" + }, + "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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/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==", + "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==" + }, + "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==", + "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==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + }, + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "engines": { + "node": ">=0.10.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==", + "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==" + }, + "node_modules/ws": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", + "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "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==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==" + }, + "@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "requires": { + "text-decoding": "^1.0.0" + } + }, + "@firebase/analytics": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.8.0.tgz", + "integrity": "sha512-wkcwainNm8Cu2xkJpDSHfhBSdDJn86Q1TZNmLWc67VrhZUHXIKXxIqb65/tNUVE+I8+sFiDDNwA+9R3MqTQTaA==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/analytics-compat": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.1.13.tgz", + "integrity": "sha512-QC1DH/Dwc8fBihn0H+jocBWyE17GF1fOCpCrpAiQ2u16F/NqsVDVG4LjIqdhq963DXaXneNY7oDwa25Up682AA==", + "requires": { + "@firebase/analytics": "0.8.0", + "@firebase/analytics-types": "0.7.0", + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/analytics-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.7.0.tgz", + "integrity": "sha512-DNE2Waiwy5+zZnCfintkDtBfaW6MjIG883474v6Z0K1XZIvl76cLND4iv0YUb48leyF+PJK1KO2XrgHb/KpmhQ==" + }, + "@firebase/app": { + "version": "0.7.28", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.28.tgz", + "integrity": "sha512-Ti0AZSDy3F5uH0Mer3dstnxGqyjaDo52E40ZRjYgxYlJXlo+LdVF8AI4OE7ZgSz6h0yPODvT2me8/ytVFSys2A==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + } + }, + "@firebase/app-check": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.5.11.tgz", + "integrity": "sha512-v+Ubf5ZDU79Pkr2q3bspBzv+NmJ3se9+2QJt37cRg00yvdjcb+RAHCLdP2abPEmOj5tZoibbm9IHxsiw1WIxLg==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/app-check-compat": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.2.11.tgz", + "integrity": "sha512-AUG1MxbbXHjRg5o3I8jK+3HRvm/CmFbMpswp0eD8Yf1EULPagn3uGArYeDQmrbD4Hvv0lsngweTSLB9BbYp6Jg==", + "requires": { + "@firebase/app-check": "0.5.11", + "@firebase/app-check-types": "0.4.0", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/app-check-interop-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.1.0.tgz", + "integrity": "sha512-uZfn9s4uuRsaX5Lwx+gFP3B6YsyOKUE+Rqa6z9ojT4VSRAsZFko9FRn6OxQUA1z5t5d08fY4pf+/+Dkd5wbdbA==" + }, + "@firebase/app-check-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.4.0.tgz", + "integrity": "sha512-SsWafqMABIOu7zLgWbmwvHGOeQQVQlwm42kwwubsmfLmL4Sf5uGpBfDhQ0CAkpi7bkJ/NwNFKafNDL9prRNP0Q==" + }, + "@firebase/app-compat": { + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.29.tgz", + "integrity": "sha512-plkKiG6sGRfh1APWSfF7FeDF79zB2kQ/Y1M1Vy7IDT6rvZhK0+ol0j7Uad2t3cpd4j615dkLIKyiG4A7RojKuw==", + "requires": { + "@firebase/app": "0.7.28", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/app-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + }, + "@firebase/auth": { + "version": "0.20.5", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.20.5.tgz", + "integrity": "sha512-SbKj7PCAuL0lXEToUOoprc1im2Lr/bzOePXyPC7WWqVgdVBt0qovbfejlzKYwJLHUAPg9UW1y3XYe3IlbXr77w==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "selenium-webdriver": "4.1.2", + "tslib": "^2.1.0" + } + }, + "@firebase/auth-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.18.tgz", + "integrity": "sha512-Fw2PJS0G/tGrfyEBcYJQ42sfy5+sANrK5xd7tuzgV7zLFW5rYkHUIZngXjuOBwLOcfO2ixa/FavfeJle3oJ38Q==", + "requires": { + "@firebase/auth": "0.20.5", + "@firebase/auth-types": "0.11.0", + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "selenium-webdriver": "4.1.2", + "tslib": "^2.1.0" + } + }, + "@firebase/auth-interop-types": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", + "requires": {} + }, + "@firebase/auth-types": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.11.0.tgz", + "integrity": "sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==", + "requires": {} + }, + "@firebase/component": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.17.tgz", + "integrity": "sha512-mTM5CBSIlmI+i76qU4+DhuExnWtzcPS3cVgObA3VAjliPPr3GrUlTaaa8KBGfxsD27juQxMsYA0TvCR5X+GQ3Q==", + "requires": { + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.2.tgz", + "integrity": "sha512-Y1LZR1LIQM8YKMkeUPpAq3/e53hcfcXO+JEZ6vCzBeD6xRawqmpw6B5/DzePdCNNvjcqheXzSaR7T39eRZo/wA==", + "requires": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.7", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "requires": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@firebase/database-compat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.2.tgz", + "integrity": "sha512-sV32QIRSNIBj/6OYtpmPzA/SfQz1/NBZbhxg9dIhGaSt9e5HaMxXRuz2lImudX0Sd/v8DKdExrxa++K6rKrRtA==", + "requires": { + "@firebase/component": "0.5.7", + "@firebase/database": "0.12.2", + "@firebase/database-types": "0.9.1", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "requires": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@firebase/database-types": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.1.tgz", + "integrity": "sha512-RUixK/YrbpxbfdE+nYP0wMcEsz1xPTnafP0q3UlSS/+fW744OITKtR1J0cMRaXbvY7EH0wUVTNVkrtgxYY8IgQ==", + "requires": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.4.0" + }, + "dependencies": { + "@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@firebase/firestore": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.4.12.tgz", + "integrity": "sha512-EILCg3GFImeRd82fMq+sHMaEAW1PRdzzkEcVcG0B5rNokyTbGE/8xLs5Q+2mIZhiWEmE6U4yNmvXcBwarTtdgA==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "@firebase/webchannel-wrapper": "0.6.2", + "@grpc/grpc-js": "^1.3.2", + "@grpc/proto-loader": "^0.6.0", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + } + }, + "@firebase/firestore-compat": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.1.21.tgz", + "integrity": "sha512-Zb+HkjG+xE2ubVmJNN2zi7aE3hFKzfqdhg0rQZGOuPn7pOaJqsKHFlGESLJ5R/TRh7I6GfK6Oniwbimjy5ILbg==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/firestore": "3.4.12", + "@firebase/firestore-types": "2.5.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/firestore-types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.0.tgz", + "integrity": "sha512-I6c2m1zUhZ5SH0cWPmINabDyH5w0PPFHk2UHsjBpKdZllzJZ2TwTkXbDtpHUZNmnc/zAa0WNMNMvcvbb/xJLKA==", + "requires": {} + }, + "@firebase/functions": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.8.4.tgz", + "integrity": "sha512-o1bB0xMyQKe+b246zGnjwHj4R6BH4mU2ZrSaa/3QvTpahUQ3hqYfkZPLOXCU7+vEFxHb3Hd4UUjkFhxoAcPqLA==", + "requires": { + "@firebase/app-check-interop-types": "0.1.0", + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.17", + "@firebase/messaging-interop-types": "0.1.0", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + } + }, + "@firebase/functions-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.2.4.tgz", + "integrity": "sha512-Crfn6il1yXGuXkjSd8nKrqR4XxPvuP19g64bXpM6Ix67qOkQg676kyOuww0FF17xN0NSXHfG8Pyf+CUrx8wJ5g==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/functions": "0.8.4", + "@firebase/functions-types": "0.5.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/functions-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.5.0.tgz", + "integrity": "sha512-qza0M5EwX+Ocrl1cYI14zoipUX4gI/Shwqv0C1nB864INAD42Dgv4v94BCyxGHBg2kzlWy8PNafdP7zPO8aJQA==" + }, + "@firebase/installations": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.5.12.tgz", + "integrity": "sha512-Zq43fCE0PB5tGJ3ojzx5RNQzKdej1188qgAk22rwjuhP7npaG/PlJqDG1/V0ZjTLRePZ1xGrfXSPlA17c/vtNw==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + } + }, + "@firebase/installations-compat": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.1.12.tgz", + "integrity": "sha512-BIhFpWIn/GkuOa+jnXkp3SDJT2RLYJF6MWpinHIBKFJs7MfrgYZ3zQ1AlhobDEql+bkD1dK4dB5sNcET2T+EyA==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/installations-types": "0.4.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/installations-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.4.0.tgz", + "integrity": "sha512-nXxWKQDvBGctuvsizbUEJKfxXU9WAaDhon+j0jpjIfOJkvkj3YHqlLB/HeYjpUn85Pb22BjplpTnDn4Gm9pc3A==", + "requires": {} + }, + "@firebase/logger": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.3.tgz", + "integrity": "sha512-POTJl07jOKTOevLXrTvJD/VZ0M6PnJXflbAh5J9VGkmtXPXNG6MdZ9fmRgqYhXKTaDId6AQenQ262uwgpdtO0Q==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/messaging": { + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.9.16.tgz", + "integrity": "sha512-Yl9gGrAvJF6C1gg3+Cr2HxlL6APsDEkrorkFafmSP1l+rg1epZKoOAcKJbSF02Vtb50wfb9FqGGy8tzodgETxg==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/messaging-interop-types": "0.1.0", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + } + }, + "@firebase/messaging-compat": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.1.16.tgz", + "integrity": "sha512-uG7rWcXJzU8vvlEBFpwG1ndw/GURrrmKcwsHopEWbsPGjMRaVWa7XrdKbvIR7IZohqPzcC/V9L8EeqF4Q4lz8w==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/messaging": "0.9.16", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/messaging-interop-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.1.0.tgz", + "integrity": "sha512-DbvUl/rXAZpQeKBnwz0NYY5OCqr2nFA0Bj28Fmr3NXGqR4PAkfTOHuQlVtLO1Nudo3q0HxAYLa68ZDAcuv2uKQ==" + }, + "@firebase/performance": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.5.12.tgz", + "integrity": "sha512-MPVTkOkGrm2SMQgI1FPNBm85y2pPqlPb6VDjIMCWkVpAr6G1IZzUT24yEMySRcIlK/Hh7/Qu1Nu5ASRzRuX6+Q==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/performance-compat": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.1.12.tgz", + "integrity": "sha512-IBORzUeGY1MGdZnsix9Mu5z4+C3WHIwalu0usxvygL0EZKHztGG8bppYPGH/b5vvg8QyHs9U+Pn1Ot2jZhffQQ==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/performance": "0.5.12", + "@firebase/performance-types": "0.1.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/performance-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.1.0.tgz", + "integrity": "sha512-6p1HxrH0mpx+622Ql6fcxFxfkYSBpE3LSuwM7iTtYU2nw91Hj6THC8Bc8z4nboIq7WvgsT/kOTYVVZzCSlXl8w==" + }, + "@firebase/polyfill": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@firebase/polyfill/-/polyfill-0.3.36.tgz", + "integrity": "sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg==", + "requires": { + "core-js": "3.6.5", + "promise-polyfill": "8.1.3", + "whatwg-fetch": "2.0.4" + } + }, + "@firebase/remote-config": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.3.11.tgz", + "integrity": "sha512-qA84dstrvVpO7rWT/sb2CLv1kjHVmz59SRFPKohJJYFBcPOGK4Pe4FWWhKAE9yg1Gnl0qYAGkahOwNawq3vE0g==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/installations": "0.5.12", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/remote-config-compat": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.1.12.tgz", + "integrity": "sha512-Yz7Gtb2rLa7ykXZX9DnSTId8CXd++jFFLW3foUImrYwJEtWgLJc7gwkRfd1M73IlKGNuQAY+DpUNF0n1dLbecA==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/remote-config": "0.3.11", + "@firebase/remote-config-types": "0.2.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/remote-config-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.2.0.tgz", + "integrity": "sha512-hqK5sCPeZvcHQ1D6VjJZdW6EexLTXNMJfPdTwbD8NrXUw6UjWC4KWhLK/TSlL0QPsQtcKRkaaoP+9QCgKfMFPw==" + }, + "@firebase/storage": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.9.9.tgz", + "integrity": "sha512-Zch7srLT2SIh9y2nCVv/4Kne0HULn7OPkmreY70BJTUJ+g5WLRjggBq6x9fV5ls9V38iqMWfn4prxzX8yIc08A==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/util": "1.6.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + } + }, + "@firebase/storage-compat": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.1.17.tgz", + "integrity": "sha512-nOYmnpI0gwoz5nROseMi9WbmHGf+xumfsOvdPyMZAjy0VqbDnpKIwmTUZQBdR+bLuB5oIkHQsvw9nbb1SH+PzQ==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/storage": "0.9.9", + "@firebase/storage-types": "0.6.0", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/storage-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.6.0.tgz", + "integrity": "sha512-1LpWhcCb1ftpkP/akhzjzeFxgVefs6eMD2QeKiJJUGH1qOiows2w5o0sKCUSQrvrRQS1lz3SFGvNR1Ck/gqxeA==", + "requires": {} + }, + "@firebase/util": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.6.3.tgz", + "integrity": "sha512-FujteO6Zjv6v8A4HS+t7c+PjU0Kaxj+rOnka0BsI/twUaCC9t8EQPmXpWZdk7XfszfahJn2pqsflUWUhtUkRlg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/webchannel-wrapper": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.2.tgz", + "integrity": "sha512-zThUKcqIU6utWzM93uEvhlh8qj8A5LMPFJPvk/ODb+8GSSif19xM2Lw1M2ijyBy8+6skSkQBbavPzOU5Oh/8tQ==" + }, + "@google-cloud/firestore": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.1.tgz", + "integrity": "sha512-5q4sl1XCL8NH2y82KZ4WQGHDOPnrSMYq3JpIeKD5C0OCSb4MfckOTB9LeAQ3p5tlL+7UsVRHj0SyzKz27XZJjw==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.1", + "protobufjs": "^7.0.0" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + }, + "protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "optional": true, + "requires": { + "@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" + } + } + } + }, + "@google-cloud/paginator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.0.tgz", + "integrity": "sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ==", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/precise-date": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.4.tgz", + "integrity": "sha512-nOB+mZdevI/1Si0QAfxWfzzIqFdc7wrO+DYePFvgbOoMtvX+XfFTINNt7e9Zg66AbDbWCPRnikU+6f5LTm9Wyg==" + }, + "@google-cloud/projectify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==" + }, + "@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==" + }, + "@google-cloud/pubsub": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-3.0.1.tgz", + "integrity": "sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA==", + "requires": { + "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^8.0.2", + "google-gax": "^3.0.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + } + }, + "@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "requires": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "dependencies": { + "@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true + }, + "@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true + } + } + }, + "@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "dependencies": { + "@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + } + }, + "protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + } + } + } + } + }, + "@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + } + }, + "@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "requires": { + "lodash": "^4.17.21" + } + }, + "@opentelemetry/api": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.1.0.tgz", + "integrity": "sha512-hf+3bwuBwtXsugA2ULBc95qxrOqP2pOekLz34BJhcAKawt94vfeNyUKpYc0lZQ/3sCP6LqRa7UAdHA7i5UODzQ==" + }, + "@opentelemetry/semantic-conventions": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.4.0.tgz", + "integrity": "sha512-Hzl8soGpmyzja9w3kiFFcYJ7n5HNETpplY6cb67KR4QPlxp4FTTresO06qXHgHDhyIInmbLJXuwARjjpsKYGuQ==" + }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, + "@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==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "@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==", + "optional": true + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "@types/duplexify": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.1.tgz", + "integrity": "sha512-n0zoEj/fMdMOvqbHxmqnza/kXyoGgJmEpsXjpP+gEqE1Ye4yNqc7xWipKnUoMpWhMuzJQSfK2gMrwlElly7OGQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.29", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", + "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "requires": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "requires": { + "@types/node": "*" + } + }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, + "@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + }, + "@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "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==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "requires": {} + }, + "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==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "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==", + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "requires": { + "lodash": "^4.17.15" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "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==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "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==", + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "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==" + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==" + }, + "espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "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==" + }, + "express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "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==", + "optional": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fast-text-encoding": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.4.tgz", + "integrity": "sha512-x6lDDm/tBAzX9kmsPcZsNbvDs3Zey3+scsxaZElS8xWLgUMAg/oFLeewfUz0mu1CblHhhsu15jGkraldkFh8KQ==" + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "firebase": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.9.0.tgz", + "integrity": "sha512-EEUUEDcOTMlNMg4blQQrTmt9axWVI5GNL6XYgrK8kKZsgi8BAUP88b2AWW+UIb684Z/yNENMcRWaghOjOV4rSg==", + "requires": { + "@firebase/analytics": "0.8.0", + "@firebase/analytics-compat": "0.1.13", + "@firebase/app": "0.7.28", + "@firebase/app-check": "0.5.11", + "@firebase/app-check-compat": "0.2.11", + "@firebase/app-compat": "0.1.29", + "@firebase/app-types": "0.7.0", + "@firebase/auth": "0.20.5", + "@firebase/auth-compat": "0.2.18", + "@firebase/database": "0.13.3", + "@firebase/database-compat": "0.2.3", + "@firebase/firestore": "3.4.12", + "@firebase/firestore-compat": "0.1.21", + "@firebase/functions": "0.8.4", + "@firebase/functions-compat": "0.2.4", + "@firebase/installations": "0.5.12", + "@firebase/installations-compat": "0.1.12", + "@firebase/messaging": "0.9.16", + "@firebase/messaging-compat": "0.1.16", + "@firebase/performance": "0.5.12", + "@firebase/performance-compat": "0.1.12", + "@firebase/polyfill": "0.3.36", + "@firebase/remote-config": "0.3.11", + "@firebase/remote-config-compat": "0.1.12", + "@firebase/storage": "0.9.9", + "@firebase/storage-compat": "0.1.17", + "@firebase/util": "1.6.3" + }, + "dependencies": { + "@firebase/database": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.3.tgz", + "integrity": "sha512-ZE+QJqQUaCTZiIzGq3RJLo64HRMtbdaEwyDhfZyPEzMJV4kyLsw3cHdEHVCtBmdasTvwtpO2YRFmd4AXAoKtNw==", + "requires": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.3.tgz", + "integrity": "sha512-uwSMnbjlSQM5gQRq8OoBLs7uc7obwsl0D6kSDAnMOlPtPl9ert79Rq9faU/COjybsJ8l7tNXMVYYJo3mQ5XNrA==", + "requires": { + "@firebase/component": "0.5.17", + "@firebase/database": "0.13.3", + "@firebase/database-types": "0.9.11", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.11.tgz", + "integrity": "sha512-27V3eFomWCZqLR6qb3Q9eS2lsUtulhSHeDNaL6fImwnhvMYTmf6ZwMfRWupgi8AFwW4s91g9Oc1/fkQtJGHKQw==", + "requires": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.6.3" + } + } + } + }, + "firebase-admin": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.4.1.tgz", + "integrity": "sha512-t5+Pf8rC01TW1KPD5U8Q45AEn7eK+FJaHlpzYStFb62J+MQmN/kB/PWUEsNn+7MNAQ0DZxFUCgJoi+bRmf83oQ==", + "requires": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.6", + "@firebase/database-types": "^0.9.13", + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^2.1.4", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==" + }, + "@firebase/auth-interop-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", + "requires": {} + }, + "@firebase/component": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", + "requires": { + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", + "requires": { + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", + "requires": { + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", + "requires": { + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" + } + }, + "@firebase/logger": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "firebase-functions": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "lodash": "^4.17.14", + "node-fetch": "^2.6.7" + } + }, + "firebase-functions-test": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.2.3.tgz", + "integrity": "sha512-zYX0QTm53wCazuej7O0xqbHl90r/v1PTXt/hwa0jo1YF8nDM+iBKnLDlkIoW66MDd0R6aGg4BvKzTTdJpvigUA==", + "dev": true, + "requires": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "gaxios": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.1.tgz", + "integrity": "sha512-keK47BGKHyyOVQxgcUaSaFvr3ehZYAlvhvpHXy0YB2itzZef+GqZR8TBsfVRWghdwlKrYsn+8L8i3eblF7Oviw==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "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==" + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "google-auth-library": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.1.0.tgz", + "integrity": "sha512-J/fNXEnqLgbr3kmeUshZCtHQia6ZiNbbrebVzpt/+LTeY6Ka9CtbQvloTjVGVO7nyYbs0KYeuIwgUC/t2Gp1Jw==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.0.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "requires": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "dependencies": { + "@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "proto3-json-serializer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.0.2.tgz", + "integrity": "sha512-wHxf8jYZ/LUP3M7XmULDKnbxBn+Bvk6SM+tDCPVTp9vraIzUi9hHsOBb1n2Y0VV0ukx4zBN/2vzMQYs4KWwRpg==", + "requires": { + "protobufjs": "^6.11.3" + }, + "dependencies": { + "protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "requires": { + "@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/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + } + } + }, + "protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + } + } + }, + "protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "requires": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + } + } + } + }, + "google-p12-pem": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.0.tgz", + "integrity": "sha512-lRTMn5ElBdDixv4a86bixejPSRk1boRtUowNepeKEVvYiFlkLuAJUVpEz6PfObDHYEKnZWq/9a2zC98xu62A9w==", + "requires": { + "node-forge": "^1.3.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "gtoken": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.0.tgz", + "integrity": "sha512-WPZcFw34wh2LUvbCUWI70GDhOlO7qHpSvFHFqq7d3Wvsf8dIJedE0lnUdOmsKuC0NgflKmF0LxIF38vsGeHHiQ==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "dependencies": { + "gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "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==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "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==" + }, + "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==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "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==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==" + }, + "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==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "requires": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + } + }, + "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==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jszip": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz", + "integrity": "sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz", + "integrity": "sha512-mpArfgPkUpX11lNtGxsF/szkasUcbWHGplZl/uFvFO2NuMHmt0dQXIihh0rkPU2yQd5niQtuUHbXnG/WKiXF6Q==", + "requires": { + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^8.5.8", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "dependencies": { + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "requires": { + "uc.micro": "^1.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==" + }, + "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==" + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "requires": {} + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==" + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "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==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "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==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==" + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promise-polyfill": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz", + "integrity": "sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==" + }, + "protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "requires": { + "@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/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, + "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==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "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==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "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==" + }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "requires": { + "lodash": "^4.17.21" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true + }, + "retry-request": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.1.tgz", + "integrity": "sha512-lxFKrlBt0OZzCWh/V0uPEN0vlr3OhdeXnpeY5OES+ckslm791Cb1D5P7lJUSnY7J5hiCjcyaUGmzCnIGDCUBig==", + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "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==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "selenium-webdriver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz", + "integrity": "sha512-e4Ap8vQvhipgBB8Ry9zBiKGkU6kHKyNnWiavGGLKkrdW81Zv7NVMtFOL/j3yX0G8QScM7XIXijKssNd4EUxSOw==", + "requires": { + "jszip": "^3.6.0", + "tmp": "^0.2.1", + "ws": ">=7.4.6" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "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" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "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==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "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==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + } + }, + "text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" + }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "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==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "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==" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "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==" + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, + "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==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", + "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", + "requires": {} + }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + }, + "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==", + "optional": true + } + } +} diff --git a/scripts/triggers-end-to-end-tests/triggers/package.json b/scripts/triggers-end-to-end-tests/triggers/package.json new file mode 100644 index 00000000000..6911ca48e2c --- /dev/null +++ b/scripts/triggers-end-to-end-tests/triggers/package.json @@ -0,0 +1,19 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": {}, + "engines": { + "node": "20" + }, + "dependencies": { + "@firebase/database-compat": "0.1.2", + "@google-cloud/pubsub": "^3.0.1", + "firebase": "^9.9.0", + "firebase-admin": "^11.0.0", + "firebase-functions": "^3.24.1" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "private": true +} diff --git a/scripts/triggers-end-to-end-tests/v1/.gitignore b/scripts/triggers-end-to-end-tests/v1/.gitignore new file mode 100644 index 00000000000..884afa60ceb --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v1/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.eslintrc +package-lock.json diff --git a/scripts/triggers-end-to-end-tests/v1/index.js b/scripts/triggers-end-to-end-tests/v1/index.js new file mode 100644 index 00000000000..0d7e0f84f5e --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v1/index.js @@ -0,0 +1,171 @@ +const admin = require("firebase-admin"); +const functions = require("firebase-functions"); + +/* + * Log snippets that the driver program above checks for. Be sure to update + * ../test.js if you plan on changing these. + */ +const RTDB_FUNCTION_LOG = "========== RTDB FUNCTION =========="; +const FIRESTORE_FUNCTION_LOG = "========== FIRESTORE FUNCTION =========="; +const PUBSUB_FUNCTION_LOG = "========== PUBSUB FUNCTION =========="; +const AUTH_FUNCTION_LOG = "========== AUTH FUNCTION =========="; +const STORAGE_FUNCTION_ARCHIVED_LOG = "========== STORAGE FUNCTION ARCHIVED =========="; +const STORAGE_FUNCTION_DELETED_LOG = "========== STORAGE FUNCTION DELETED =========="; +const STORAGE_FUNCTION_FINALIZED_LOG = "========== STORAGE FUNCTION FINALIZED =========="; +const STORAGE_FUNCTION_METADATA_LOG = "========== STORAGE FUNCTION METADATA =========="; +const STORAGE_BUCKET_FUNCTION_ARCHIVED_LOG = + "========== STORAGE BUCKET FUNCTION ARCHIVED =========="; +const STORAGE_BUCKET_FUNCTION_DELETED_LOG = "========== STORAGE BUCKET FUNCTION DELETED =========="; +const STORAGE_BUCKET_FUNCTION_FINALIZED_LOG = + "========== STORAGE BUCKET FUNCTION FINALIZED =========="; +const STORAGE_BUCKET_FUNCTION_METADATA_LOG = + "========== STORAGE BUCKET FUNCTION METADATA =========="; + +/* + * We install onWrite triggers for START_DOCUMENT_NAME in both the firestore and + * database emulators. From each respective onWrite trigger, we write a document + * to both the firestore and database emulators. This exercises the + * bidirectional communication between cloud functions and each emulator. + */ +const START_DOCUMENT_NAME = "test/start"; +const END_DOCUMENT_NAME = "test/done"; + +const PUBSUB_TOPIC = "test-topic"; + +admin.initializeApp(); + +exports.firestoreReaction = functions.firestore + .document(START_DOCUMENT_NAME) + .onWrite(async (/* change, ctx */) => { + console.log(FIRESTORE_FUNCTION_LOG); + /* + * Write back a completion timestamp to the firestore emulator. The test + * driver program checks for this by querying the firestore emulator + * directly. + */ + const ref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_firestore"); + await ref.set({ done: new Date().toISOString() }); + + /* + * Write a completion marker to the firestore emulator. This exercise + * cross-emulator communication. + */ + const dbref = admin.database().ref(END_DOCUMENT_NAME + "_from_firestore"); + await dbref.set({ done: new Date().toISOString() }); + + return true; + }); + +exports.rtdbReaction = functions.database + .ref(START_DOCUMENT_NAME) + .onWrite(async (/* change, ctx */) => { + console.log(RTDB_FUNCTION_LOG); + + const ref = admin.database().ref(END_DOCUMENT_NAME + "_from_database"); + await ref.set({ done: new Date().toISOString() }); + + const firestoreref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_database"); + await firestoreref.set({ done: new Date().toISOString() }); + + return true; + }); + +exports.pubsubReaction = functions.pubsub.topic(PUBSUB_TOPIC).onPublish((msg /* , ctx */) => { + console.log(PUBSUB_FUNCTION_LOG); + console.log("Message", JSON.stringify(msg.json)); + console.log("Attributes", JSON.stringify(msg.attributes)); + return true; +}); + +exports.pubsubScheduled = functions.pubsub.schedule("every mon 07:00").onRun((context) => { + console.log(PUBSUB_FUNCTION_LOG); + console.log("Resource", JSON.stringify(context.resource)); + return true; +}); + +exports.authReaction = functions.auth.user().onCreate((user, ctx) => { + console.log(AUTH_FUNCTION_LOG); + console.log("User", JSON.stringify(user)); + return true; +}); + +exports.storageArchiveReaction = functions.storage + .bucket() + .object() + .onArchive((object, context) => { + console.log(STORAGE_FUNCTION_ARCHIVED_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.storageDeleteReaction = functions.storage + .bucket() + .object() + .onDelete((object, context) => { + console.log(STORAGE_FUNCTION_DELETED_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.storageFinalizeReaction = functions.storage + .bucket() + .object() + .onFinalize((object, context) => { + console.log(STORAGE_FUNCTION_FINALIZED_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.storageMetadataReaction = functions.storage + .bucket() + .object() + .onMetadataUpdate((object, context) => { + console.log(STORAGE_FUNCTION_METADATA_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.onCall = functions.https.onCall((data) => { + console.log("data", JSON.stringify(data)); + return data; +}); + +exports.storageBucketArchiveReaction = functions.storage + .bucket("test-bucket") + .object() + .onArchive((object, context) => { + console.log(STORAGE_BUCKET_FUNCTION_ARCHIVED_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.storageBucketDeleteReaction = functions.storage + .bucket("test-bucket") + .object() + .onDelete((object, context) => { + console.log(STORAGE_BUCKET_FUNCTION_DELETED_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.storageBucketFinalizeReaction = functions.storage + .bucket("test-bucket") + .object() + .onFinalize((object, context) => { + console.log(STORAGE_BUCKET_FUNCTION_FINALIZED_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.storageBucketMetadataReaction = functions.storage + .bucket("test-bucket") + .object() + .onMetadataUpdate((object, context) => { + console.log(STORAGE_BUCKET_FUNCTION_METADATA_LOG); + console.log("Object", JSON.stringify(object)); + return true; + }); + +exports.onReq = functions.https.onRequest((req, res) => { + res.send("onReq"); +}); diff --git a/scripts/triggers-end-to-end-tests/v1/package-lock.json b/scripts/triggers-end-to-end-tests/v1/package-lock.json new file mode 100644 index 00000000000..aeff6b4d8cf --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v1/package-lock.json @@ -0,0 +1,5376 @@ +{ + "name": "functions", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "@firebase/database-compat": "0.1.2", + "firebase-admin": "^11.0.0", + "firebase-functions": "^3.24.1" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "optional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@firebase/app": { + "version": "0.7.28", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.28.tgz", + "integrity": "sha512-Ti0AZSDy3F5uH0Mer3dstnxGqyjaDo52E40ZRjYgxYlJXlo+LdVF8AI4OE7ZgSz6h0yPODvT2me8/ytVFSys2A==", + "peer": true, + "dependencies": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-compat": { + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.29.tgz", + "integrity": "sha512-plkKiG6sGRfh1APWSfF7FeDF79zB2kQ/Y1M1Vy7IDT6rvZhK0+ol0j7Uad2t3cpd4j615dkLIKyiG4A7RojKuw==", + "peer": true, + "dependencies": { + "@firebase/app": "0.7.28", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.17.tgz", + "integrity": "sha512-mTM5CBSIlmI+i76qU4+DhuExnWtzcPS3cVgObA3VAjliPPr3GrUlTaaa8KBGfxsD27juQxMsYA0TvCR5X+GQ3Q==", + "peer": true, + "dependencies": { + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.2.tgz", + "integrity": "sha512-Y1LZR1LIQM8YKMkeUPpAq3/e53hcfcXO+JEZ6vCzBeD6xRawqmpw6B5/DzePdCNNvjcqheXzSaR7T39eRZo/wA==", + "dependencies": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.7", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.2.tgz", + "integrity": "sha512-sV32QIRSNIBj/6OYtpmPzA/SfQz1/NBZbhxg9dIhGaSt9e5HaMxXRuz2lImudX0Sd/v8DKdExrxa++K6rKrRtA==", + "dependencies": { + "@firebase/component": "0.5.7", + "@firebase/database": "0.12.2", + "@firebase/database-types": "0.9.1", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "dependencies": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.1.tgz", + "integrity": "sha512-RUixK/YrbpxbfdE+nYP0wMcEsz1xPTnafP0q3UlSS/+fW744OITKtR1J0cMRaXbvY7EH0wUVTNVkrtgxYY8IgQ==", + "dependencies": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.4.0" + } + }, + "node_modules/@firebase/database-types/node_modules/@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database/node_modules/@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "dependencies": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database/node_modules/@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database/node_modules/@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/logger": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.3.tgz", + "integrity": "sha512-POTJl07jOKTOevLXrTvJD/VZ0M6PnJXflbAh5J9VGkmtXPXNG6MdZ9fmRgqYhXKTaDId6AQenQ262uwgpdtO0Q==", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.6.3.tgz", + "integrity": "sha512-FujteO6Zjv6v8A4HS+t7c+PjU0Kaxj+rOnka0BsI/twUaCC9t8EQPmXpWZdk7XfszfahJn2pqsflUWUhtUkRlg==", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "optional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "engines": { + "node": ">=10.13.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==", + "optional": true + }, + "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==", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "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==", + "optional": true, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.29", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", + "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "optional": true + }, + "node_modules/@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "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==", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "optional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "node_modules/@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "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==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "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==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "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==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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" + } + ], + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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==", + "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==", + "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==", + "optional": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "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==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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==" + }, + "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==", + "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==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "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==" + }, + "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==", + "optional": true + }, + "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==", + "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==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "optional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "optional": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "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==", + "optional": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true + }, + "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==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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": "11.4.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.4.1.tgz", + "integrity": "sha512-t5+Pf8rC01TW1KPD5U8Q45AEn7eK+FJaHlpzYStFb62J+MQmN/kB/PWUEsNn+7MNAQ0DZxFUCgJoi+bRmf83oQ==", + "dependencies": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.6", + "@firebase/database-types": "^0.9.13", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^2.1.4", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==" + }, + "node_modules/firebase-admin/node_modules/@firebase/auth-interop-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/component": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", + "dependencies": { + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", + "dependencies": { + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", + "dependencies": { + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database-types": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", + "dependencies": { + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/logger": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/util": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-functions": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "lodash": "^4.17.14", + "node-fetch": "^2.6.7" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.2.3.tgz", + "integrity": "sha512-zYX0QTm53wCazuej7O0xqbHl90r/v1PTXt/hwa0jo1YF8nDM+iBKnLDlkIoW66MDd0R6aGg4BvKzTTdJpvigUA==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "firebase-admin": ">=6.0.0", + "firebase-functions": ">=2.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==" + }, + "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==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "peer": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "node_modules/jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "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==", + "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==", + "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==" + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz", + "integrity": "sha512-mpArfgPkUpX11lNtGxsF/szkasUcbWHGplZl/uFvFO2NuMHmt0dQXIihh0rkPU2yQd5niQtuUHbXnG/WKiXF6Q==", + "dependencies": { + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^8.5.8", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=10 < 13 || >=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "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==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "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==", + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "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==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "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==", + "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==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "optional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "optional": true, + "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/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "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==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "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==", + "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==", + "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==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "optional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "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==", + "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==" + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "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==", + "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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/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==", + "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==" + }, + "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==", + "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==", + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true, + "engines": { + "node": ">=0.10.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==", + "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==", + "optional": true + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "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==", + "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==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "optional": true + }, + "@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "requires": { + "text-decoding": "^1.0.0" + } + }, + "@firebase/app": { + "version": "0.7.28", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.28.tgz", + "integrity": "sha512-Ti0AZSDy3F5uH0Mer3dstnxGqyjaDo52E40ZRjYgxYlJXlo+LdVF8AI4OE7ZgSz6h0yPODvT2me8/ytVFSys2A==", + "peer": true, + "requires": { + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + } + }, + "@firebase/app-compat": { + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.29.tgz", + "integrity": "sha512-plkKiG6sGRfh1APWSfF7FeDF79zB2kQ/Y1M1Vy7IDT6rvZhK0+ol0j7Uad2t3cpd4j615dkLIKyiG4A7RojKuw==", + "peer": true, + "requires": { + "@firebase/app": "0.7.28", + "@firebase/component": "0.5.17", + "@firebase/logger": "0.3.3", + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/app-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + }, + "@firebase/auth-interop-types": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==", + "requires": {} + }, + "@firebase/component": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.17.tgz", + "integrity": "sha512-mTM5CBSIlmI+i76qU4+DhuExnWtzcPS3cVgObA3VAjliPPr3GrUlTaaa8KBGfxsD27juQxMsYA0TvCR5X+GQ3Q==", + "peer": true, + "requires": { + "@firebase/util": "1.6.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.2.tgz", + "integrity": "sha512-Y1LZR1LIQM8YKMkeUPpAq3/e53hcfcXO+JEZ6vCzBeD6xRawqmpw6B5/DzePdCNNvjcqheXzSaR7T39eRZo/wA==", + "requires": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.7", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "requires": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@firebase/database-compat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.2.tgz", + "integrity": "sha512-sV32QIRSNIBj/6OYtpmPzA/SfQz1/NBZbhxg9dIhGaSt9e5HaMxXRuz2lImudX0Sd/v8DKdExrxa++K6rKrRtA==", + "requires": { + "@firebase/component": "0.5.7", + "@firebase/database": "0.12.2", + "@firebase/database-types": "0.9.1", + "@firebase/logger": "0.3.0", + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.7.tgz", + "integrity": "sha512-CiAHUPXh2hn/lpzMShNmfAxHNQhKQwmQUJSYMPCjf2bCCt4Z2vLGpS+UWEuNFm9Zf8LNmkS+Z+U/s4Obi5carg==", + "requires": { + "@firebase/util": "1.4.0", + "tslib": "^2.1.0" + } + }, + "@firebase/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@firebase/database-types": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.1.tgz", + "integrity": "sha512-RUixK/YrbpxbfdE+nYP0wMcEsz1xPTnafP0q3UlSS/+fW744OITKtR1J0cMRaXbvY7EH0wUVTNVkrtgxYY8IgQ==", + "requires": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.4.0" + }, + "dependencies": { + "@firebase/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.0.tgz", + "integrity": "sha512-Qn58d+DVi1nGn0bA9RV89zkz0zcbt6aUcRdyiuub/SuEvjKYstWmHcHwh1C0qmE1wPf9a3a+AuaRtduaGaRT7A==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@firebase/logger": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.3.tgz", + "integrity": "sha512-POTJl07jOKTOevLXrTvJD/VZ0M6PnJXflbAh5J9VGkmtXPXNG6MdZ9fmRgqYhXKTaDId6AQenQ262uwgpdtO0Q==", + "peer": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.6.3.tgz", + "integrity": "sha512-FujteO6Zjv6v8A4HS+t7c+PjU0Kaxj+rOnka0BsI/twUaCC9t8EQPmXpWZdk7XfszfahJn2pqsflUWUhtUkRlg==", + "peer": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + } + }, + "@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true + }, + "@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true + }, + "@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "requires": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true + } + } + }, + "@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + } + }, + "@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, + "@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==", + "optional": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "optional": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "optional": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "optional": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "optional": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "optional": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "optional": true + }, + "@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==", + "optional": true + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.29", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", + "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "requires": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "requires": { + "@types/node": "*" + } + }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "optional": true + }, + "@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "optional": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "optional": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "optional": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "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==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "optional": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "requires": {} + }, + "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==", + "optional": true, + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "optional": true + }, + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "optional": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "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==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "optional": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "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==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "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==", + "optional": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "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==", + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "optional": true + }, + "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==" + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "optional": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "optional": true + }, + "espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "optional": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "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==", + "optional": true + }, + "express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "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==", + "optional": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "firebase-admin": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.4.1.tgz", + "integrity": "sha512-t5+Pf8rC01TW1KPD5U8Q45AEn7eK+FJaHlpzYStFb62J+MQmN/kB/PWUEsNn+7MNAQ0DZxFUCgJoi+bRmf83oQ==", + "requires": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.6", + "@firebase/database-types": "^0.9.13", + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^2.1.4", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==" + }, + "@firebase/auth-interop-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", + "requires": {} + }, + "@firebase/component": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", + "requires": { + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", + "requires": { + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", + "requires": { + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", + "requires": { + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" + } + }, + "@firebase/logger": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "firebase-functions": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz", + "integrity": "sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "lodash": "^4.17.14", + "node-fetch": "^2.6.7" + } + }, + "firebase-functions-test": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.2.3.tgz", + "integrity": "sha512-zYX0QTm53wCazuej7O0xqbHl90r/v1PTXt/hwa0jo1YF8nDM+iBKnLDlkIoW66MDd0R6aGg4BvKzTTdJpvigUA==", + "dev": true, + "requires": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "optional": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "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==", + "optional": true + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "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==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "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==" + }, + "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==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "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==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "peer": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==" + }, + "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==", + "optional": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "requires": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + } + }, + "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==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz", + "integrity": "sha512-mpArfgPkUpX11lNtGxsF/szkasUcbWHGplZl/uFvFO2NuMHmt0dQXIihh0rkPU2yQd5niQtuUHbXnG/WKiXF6Q==", + "requires": { + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^8.5.8", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "dependencies": { + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "optional": true + }, + "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==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "requires": {} + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "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==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "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==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true + }, + "proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "optional": true, + "requires": { + "protobufjs": "^7.0.0" + } + }, + "protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "optional": true, + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + } + } + }, + "protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "requires": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + } + }, + "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==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "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==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "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==", + "optional": true + }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true + }, + "retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "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==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "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" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "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==", + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "optional": true + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + } + }, + "text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "optional": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "optional": true + }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true + }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "optional": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "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==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "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==" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "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==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "optional": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "requires": { + "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" + } + }, + "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==", + "optional": true + }, + "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==", + "optional": true + } + } +} diff --git a/scripts/triggers-end-to-end-tests/v1/package.json b/scripts/triggers-end-to-end-tests/v1/package.json new file mode 100644 index 00000000000..ac1bc4c3b67 --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v1/package.json @@ -0,0 +1,17 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": {}, + "engines": { + "node": "20" + }, + "dependencies": { + "@firebase/database-compat": "0.1.2", + "firebase-admin": "^11.0.0", + "firebase-functions": "^3.24.1" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "private": true +} diff --git a/scripts/triggers-end-to-end-tests/v2/.gitignore b/scripts/triggers-end-to-end-tests/v2/.gitignore new file mode 100644 index 00000000000..884afa60ceb --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v2/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.eslintrc +package-lock.json diff --git a/scripts/triggers-end-to-end-tests/v2/index.js b/scripts/triggers-end-to-end-tests/v2/index.js new file mode 100644 index 00000000000..b580a3a83e1 --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v2/index.js @@ -0,0 +1,164 @@ +const admin = require("firebase-admin"); +const functionsV2 = require("firebase-functions/v2"); + +/* + * Log snippets that the driver program above checks for. Be sure to update + * ../test.js if you plan on changing these. + */ +const PUBSUB_FUNCTION_LOG = "========== PUBSUB V2 FUNCTION =========="; +const STORAGE_FUNCTION_ARCHIVED_LOG = "========== STORAGE V2 FUNCTION ARCHIVED =========="; +const STORAGE_FUNCTION_DELETED_LOG = "========== STORAGE V2 FUNCTION DELETED =========="; +const STORAGE_FUNCTION_FINALIZED_LOG = "========== STORAGE V2 FUNCTION FINALIZED =========="; +const STORAGE_FUNCTION_METADATA_LOG = "========== STORAGE V2 FUNCTION METADATA =========="; +const STORAGE_BUCKET_FUNCTION_ARCHIVED_LOG = + "========== STORAGE BUCKET V2 FUNCTION ARCHIVED =========="; +const STORAGE_BUCKET_FUNCTION_DELETED_LOG = + "========== STORAGE BUCKET V2 FUNCTION DELETED =========="; +const STORAGE_BUCKET_FUNCTION_FINALIZED_LOG = + "========== STORAGE BUCKET V2 FUNCTION FINALIZED =========="; +const STORAGE_BUCKET_FUNCTION_METADATA_LOG = + "========== STORAGE BUCKET V2 FUNCTION METADATA =========="; +const AUTH_BLOCKING_CREATE_V2_LOG = + "========== AUTH BLOCKING CREATE V2 FUNCTION METADATA =========="; +const AUTH_BLOCKING_SIGN_IN_V2_LOG = + "========== AUTH BLOCKING SIGN IN V2 FUNCTION METADATA =========="; +const RTDB_LOG = "========== RTDB V2 FUNCTION =========="; +const FIRESTORE_LOG = "========== FIRESTORE V2 FUNCTION =========="; + +const PUBSUB_TOPIC = "test-topic"; + +const START_DOCUMENT_NAME = "test/start"; +const END_DOCUMENT_NAME = "test/done"; + +admin.initializeApp(); + +exports.httpsv2reaction = functionsV2.https.onRequest((req, res) => { + res.send("httpsv2reaction"); +}); + +exports.pubsubv2reaction = functionsV2.pubsub.onMessagePublished(PUBSUB_TOPIC, (cloudevent) => { + console.log(PUBSUB_FUNCTION_LOG); + console.log("Message", JSON.stringify(cloudevent.data.message.json)); + console.log("Attributes", JSON.stringify(cloudevent.data.message.attributes)); + return true; +}); + +exports.storagev2archivedreaction = functionsV2.storage.onObjectArchived((cloudevent) => { + console.log(STORAGE_FUNCTION_ARCHIVED_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; +}); + +exports.storagev2deletedreaction = functionsV2.storage.onObjectDeleted((cloudevent) => { + console.log(STORAGE_FUNCTION_DELETED_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; +}); + +exports.storagev2finalizedreaction = functionsV2.storage.onObjectFinalized((cloudevent) => { + console.log(STORAGE_FUNCTION_FINALIZED_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; +}); + +exports.storagev2metadatareaction = functionsV2.storage.onObjectMetadataUpdated((cloudevent) => { + console.log(STORAGE_FUNCTION_METADATA_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; +}); + +exports.storagebucketv2archivedreaction = functionsV2.storage.onObjectArchived( + "test-bucket", + (cloudevent) => { + console.log(STORAGE_BUCKET_FUNCTION_ARCHIVED_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; + }, +); + +exports.storagebucketv2deletedreaction = functionsV2.storage.onObjectDeleted( + "test-bucket", + (cloudevent) => { + console.log(STORAGE_BUCKET_FUNCTION_DELETED_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; + }, +); + +exports.storagebucketv2finalizedreaction = functionsV2.storage.onObjectFinalized( + "test-bucket", + (cloudevent) => { + console.log(STORAGE_BUCKET_FUNCTION_FINALIZED_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; + }, +); + +exports.storagebucketv2metadatareaction = functionsV2.storage.onObjectMetadataUpdated( + "test-bucket", + (cloudevent) => { + console.log(STORAGE_BUCKET_FUNCTION_METADATA_LOG); + console.log("Object", JSON.stringify(cloudevent.data)); + return true; + }, +); + +exports.oncallv2 = functionsV2.https.onCall((req) => { + console.log("data", JSON.stringify(req.data)); + return req.data; +}); + +exports.authblockingcreatereaction = functionsV2.identity.beforeUserCreated((event) => { + console.log(AUTH_BLOCKING_CREATE_V2_LOG); + return; +}); + +exports.authblockingsigninreaction = functionsV2.identity.beforeUserSignedIn((event) => { + console.log(AUTH_BLOCKING_SIGN_IN_V2_LOG); + return; +}); + +exports.onreqv2a = functionsV2.https.onRequest((req, res) => { + res.send("onreqv2a"); +}); + +exports.onreqv2b = functionsV2.https.onRequest((req, res) => { + res.send("onreqv2b"); +}); + +exports.onreqv2timeout = functionsV2.https.onRequest({ timeoutSeconds: 1 }, async (req, res) => { + return new Promise((resolve) => { + setTimeout(() => { + res.send("onreqv2timeout"); + resolve(); + }, 3_000); + }); +}); + +exports.rtdbv2reaction = functionsV2.database.onValueWritten(START_DOCUMENT_NAME, (event) => { + console.log(RTDB_LOG); + return; +}); + +exports.firestorev2reaction = functionsV2.firestore.onDocumentWritten( + START_DOCUMENT_NAME, + async (event) => { + console.log(FIRESTORE_LOG); + /* + * Write back a completion timestamp to the firestore emulator. The test + * driver program checks for this by querying the firestore emulator + * directly. + */ + const ref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_firestore"); + await ref.set({ done: new Date().toISOString() }); + + /* + * Write a completion marker to the firestore emulator. This exercise + * cross-emulator communication. + */ + const dbref = admin.database().ref(END_DOCUMENT_NAME + "_from_firestore"); + await dbref.set({ done: new Date().toISOString() }); + + return true; + }, +); diff --git a/scripts/triggers-end-to-end-tests/v2/package-lock.json b/scripts/triggers-end-to-end-tests/v2/package-lock.json new file mode 100644 index 00000000000..ba2d9587e2d --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v2/package-lock.json @@ -0,0 +1,5023 @@ +{ + "name": "functions", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^11.0.0", + "firebase-functions": "^4.3.1" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "optional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "peer": true + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", + "dependencies": { + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", + "dependencies": { + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", + "dependencies": { + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", + "dependencies": { + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" + } + }, + "node_modules/@firebase/database-types/node_modules/@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==" + }, + "node_modules/@firebase/logger": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "optional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "engines": { + "node": ">=10.13.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==" + }, + "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==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "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==", + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.29", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", + "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "optional": true + }, + "node_modules/@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "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==", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "optional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "node_modules/@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "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==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "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==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "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==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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" + } + ], + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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==", + "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==", + "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==", + "optional": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "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==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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==" + }, + "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==", + "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==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "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==" + }, + "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==", + "optional": true + }, + "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==", + "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==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "optional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "optional": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "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==", + "optional": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true + }, + "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==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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": "11.4.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.4.1.tgz", + "integrity": "sha512-t5+Pf8rC01TW1KPD5U8Q45AEn7eK+FJaHlpzYStFb62J+MQmN/kB/PWUEsNn+7MNAQ0DZxFUCgJoi+bRmf83oQ==", + "dependencies": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.6", + "@firebase/database-types": "^0.9.13", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^2.1.4", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2" + } + }, + "node_modules/firebase-functions": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.3.1.tgz", + "integrity": "sha512-sbitfzHcuWsLD03/EgeIRIfkVGeyGjNo3IEA2z+mDIkK1++LhKLCWwVQXrMqeeATOG04CAp30guAagsNElVlng==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.2.3.tgz", + "integrity": "sha512-zYX0QTm53wCazuej7O0xqbHl90r/v1PTXt/hwa0jo1YF8nDM+iBKnLDlkIoW66MDd0R6aGg4BvKzTTdJpvigUA==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "firebase-admin": ">=6.0.0", + "firebase-functions": ">=2.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==" + }, + "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==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "node_modules/jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "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==", + "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==", + "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==" + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz", + "integrity": "sha512-mpArfgPkUpX11lNtGxsF/szkasUcbWHGplZl/uFvFO2NuMHmt0dQXIihh0rkPU2yQd5niQtuUHbXnG/WKiXF6Q==", + "dependencies": { + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^8.5.8", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=10 < 13 || >=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "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==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "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==", + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "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==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "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==", + "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==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "optional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "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/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "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==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "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==", + "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==", + "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==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "optional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "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==", + "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==" + }, + "node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "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==", + "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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/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==", + "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==" + }, + "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==", + "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==", + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true, + "engines": { + "node": ">=0.10.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==", + "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==", + "optional": true + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "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==", + "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==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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==", + "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==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "optional": true + }, + "@fastify/busboy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", + "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", + "requires": { + "text-decoding": "^1.0.0" + } + }, + "@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "peer": true + }, + "@firebase/auth-interop-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.7.tgz", + "integrity": "sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==", + "requires": {} + }, + "@firebase/component": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.21.tgz", + "integrity": "sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==", + "requires": { + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.10.tgz", + "integrity": "sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==", + "requires": { + "@firebase/auth-interop-types": "0.1.7", + "@firebase/component": "0.5.21", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.10.tgz", + "integrity": "sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==", + "requires": { + "@firebase/component": "0.5.21", + "@firebase/database": "0.13.10", + "@firebase/database-types": "0.9.17", + "@firebase/logger": "0.3.4", + "@firebase/util": "1.7.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.17.tgz", + "integrity": "sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==", + "requires": { + "@firebase/app-types": "0.8.1", + "@firebase/util": "1.7.3" + }, + "dependencies": { + "@firebase/app-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.1.tgz", + "integrity": "sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==" + } + } + }, + "@firebase/logger": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.4.tgz", + "integrity": "sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.3.tgz", + "integrity": "sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@google-cloud/firestore": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.2.tgz", + "integrity": "sha512-f7xFwINJveaqTFcgy0G4o2CBPm0Gv9lTGQ4dQt+7skwaHs3ytdue9ma8oQZYXKNoWcAoDIMQ929Dk0KOIocxFg==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.2", + "protobufjs": "^7.0.0" + } + }, + "@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true + }, + "@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true + }, + "@google-cloud/storage": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.9.0.tgz", + "integrity": "sha512-0mn9DUe3dtyTWLsWLplQP3gzPolJ5kD4PwHuzeD3ye0SAQ+oFfDbT8d+vNZxqyvddL2c6uNP72TKETN2PQxDKg==", + "optional": true, + "requires": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true + } + } + }, + "@grpc/grpc-js": { + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.17.tgz", + "integrity": "sha512-DGuSbtMFbaRsyffMf+VEkVu8HkSXEUfO3UyGJNtqxW9ABdtTIA+2UXAJpwbJS+xfQxuwqLUeELmL6FuZkOqPxw==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + } + }, + "@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, + "@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==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "@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==", + "optional": true + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.29", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz", + "integrity": "sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "requires": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "requires": { + "@types/node": "*" + } + }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "optional": true + }, + "@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "optional": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "optional": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "optional": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "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==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "optional": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "requires": {} + }, + "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==", + "optional": true, + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "optional": true + }, + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "optional": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "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==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "optional": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "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==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "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==", + "optional": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "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==", + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "optional": true + }, + "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==" + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "optional": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "optional": true + }, + "espree": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "optional": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "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==", + "optional": true + }, + "express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "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==", + "optional": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "firebase-admin": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.4.1.tgz", + "integrity": "sha512-t5+Pf8rC01TW1KPD5U8Q45AEn7eK+FJaHlpzYStFb62J+MQmN/kB/PWUEsNn+7MNAQ0DZxFUCgJoi+bRmf83oQ==", + "requires": { + "@fastify/busboy": "^1.1.0", + "@firebase/database-compat": "^0.2.6", + "@firebase/database-types": "^0.9.13", + "@google-cloud/firestore": "^6.4.0", + "@google-cloud/storage": "^6.5.2", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^2.1.4", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + } + }, + "firebase-functions": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.3.1.tgz", + "integrity": "sha512-sbitfzHcuWsLD03/EgeIRIfkVGeyGjNo3IEA2z+mDIkK1++LhKLCWwVQXrMqeeATOG04CAp30guAagsNElVlng==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7", + "protobufjs": "^7.2.2" + } + }, + "firebase-functions-test": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.2.3.tgz", + "integrity": "sha512-zYX0QTm53wCazuej7O0xqbHl90r/v1PTXt/hwa0jo1YF8nDM+iBKnLDlkIoW66MDd0R6aGg4BvKzTTdJpvigUA==", + "dev": true, + "requires": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "optional": true + }, + "gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "optional": true, + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "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==", + "optional": true + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "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==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "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==" + }, + "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==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "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==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==" + }, + "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==", + "optional": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true + }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "requires": { + "xmlcreate": "^2.0.4" + } + }, + "jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "requires": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + } + }, + "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==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz", + "integrity": "sha512-mpArfgPkUpX11lNtGxsF/szkasUcbWHGplZl/uFvFO2NuMHmt0dQXIihh0rkPU2yQd5niQtuUHbXnG/WKiXF6Q==", + "requires": { + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^8.5.8", + "debug": "^4.3.4", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "dependencies": { + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==", + "optional": true + }, + "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==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "requires": {} + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "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==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "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==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "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==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true + }, + "proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "optional": true, + "requires": { + "protobufjs": "^7.0.0" + } + }, + "protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" + } + } + }, + "protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "requires": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + } + }, + "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==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "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==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "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==", + "optional": true + }, + "requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true + }, + "retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "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==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "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" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "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==", + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "optional": true + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "teeny-request": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.2.tgz", + "integrity": "sha512-34pe0a4zASseXZCKdeTiIZqSKA8ETHb1EwItZr01PAR3CLPojeAKgSjzeNS4373gi59hNulyDrPKEbh2zO9sCg==", + "optional": true, + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + } + }, + "text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "optional": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "optional": true + }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true + }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "optional": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "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==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "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==" + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "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==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true + }, + "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==", + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "optional": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "requires": { + "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" + } + }, + "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==", + "optional": true + }, + "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==", + "optional": true + } + } +} diff --git a/scripts/triggers-end-to-end-tests/v2/package.json b/scripts/triggers-end-to-end-tests/v2/package.json new file mode 100644 index 00000000000..ec05c49be7d --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v2/package.json @@ -0,0 +1,16 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": {}, + "engines": { + "node": "20" + }, + "dependencies": { + "firebase-admin": "^11.0.0", + "firebase-functions": "^4.3.1" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "private": true +} diff --git a/scripts/tweet.js b/scripts/tweet.js deleted file mode 100644 index a1016cc30f8..00000000000 --- a/scripts/tweet.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const Twitter = require("twitter"); - -function printUsage() { - console.error( - ` -Usage: tweet.js - -Credentials must be stored in "twitter.json" in this directory. - -Arguments: - - version: Version of module that was released. e.g. "1.2.3" -` - ); - process.exit(1); -} - -function getUrl(version) { - return `https://github.com/firebase/firebase-tools/releases/tag/v${version}`; -} - -if (process.argv.length !== 3) { - console.error("Missing arguments."); - printUsage(); -} - -const version = process.argv.pop(); -if (!version.match(/^\d+\.\d+\.\d+$/)) { - console.error(`Version "${version}" not a version number.`); - printUsage(); -} - -if (!fs.existsSync(`${__dirname}/twitter.json`)) { - console.error("Missing credentials."); - printUsage(); -} -const creds = require("./twitter.json"); - -const client = new Twitter(creds); - -client.post( - "statuses/update", - { status: `v${version} of @Firebase CLI is available. Release notes: ${getUrl(version)}` }, - (err) => { - if (err) { - console.error(err); - process.exit(1); - } - } -); diff --git a/scripts/webframeworks-deploy-tests/.firebaserc b/scripts/webframeworks-deploy-tests/.firebaserc new file mode 100644 index 00000000000..17a020da167 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/.firebaserc @@ -0,0 +1,18 @@ +{ + "projects": { + "default": "nextjs-demo-73e34" + }, + "targets": { + "demo-123": { + "hosting": { + "angular": [ + "demo-angular" + ], + "nextjs": [ + "demo-nextjs" + ] + } + } + }, + "etags": {} +} diff --git a/scripts/webframeworks-deploy-tests/.gitignore b/scripts/webframeworks-deploy-tests/.gitignore new file mode 100644 index 00000000000..dbb58ffbfa3 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/.gitignore @@ -0,0 +1,66 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env diff --git a/scripts/webframeworks-deploy-tests/README.md b/scripts/webframeworks-deploy-tests/README.md new file mode 100644 index 00000000000..6412edc6d53 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/README.md @@ -0,0 +1,20 @@ +# WebFrameworks Deploy Integration Test + +This integration test deploys a nextjs hosted project with webframeworks enabled. + +The test isn't "thread-safe" - there should be at most one test running on a project at any given time. +I suggest you to use your own project to run the test. + +You can set the test project and run the integration test as follows: + +```bash +$ GCLOUD_PROJECT=${PROJECT_ID} npm run test:webframeworks-deploy +``` + +The integration test blows whats being hosted! Don't run it on a project where you have functions you'd like to keep. + +You can also run the test target with `FIREBASE_DEBUG=true` to pass `--debug` flag to CLI invocation: + +```bash +$ GCLOUD_PROJECT=${PROJECT_ID} FIREBASE_DEBUG=true npm run test:webframeworks-deploy +``` diff --git a/scripts/webframeworks-deploy-tests/angular/.editorconfig b/scripts/webframeworks-deploy-tests/angular/.editorconfig new file mode 100644 index 00000000000..59d9a3a3e73 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/scripts/webframeworks-deploy-tests/angular/.gitignore b/scripts/webframeworks-deploy-tests/angular/.gitignore new file mode 100644 index 00000000000..0711527ef9d --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/scripts/webframeworks-deploy-tests/angular/README.md b/scripts/webframeworks-deploy-tests/angular/README.md new file mode 100644 index 00000000000..3033df55d4e --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/README.md @@ -0,0 +1,27 @@ +# Angular + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/scripts/webframeworks-deploy-tests/angular/angular.json b/scripts/webframeworks-deploy-tests/angular/angular.json new file mode 100644 index 00000000000..106ce9df1a7 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/angular.json @@ -0,0 +1,201 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "i18n": { + "sourceLocale": { + "code": "en", + "baseHref": "" + }, + "locales": { + "fr": { + "translation": "src/locale/messages.fr.xlf", + "baseHref": "" + }, + "es": { + "translation": "src/locale/messages.es.xlf", + "baseHref": "" + } + } + }, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "localize": true, + "outputPath": "dist/angular/browser", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "localize": ["en"], + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "angular:build:production" + }, + "development": { + "buildTarget": "angular:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "angular:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + }, + "server": { + "builder": "@angular-devkit/build-angular:server", + "options": { + "localize": true, + "outputPath": "dist/angular/server", + "main": "server.ts", + "tsConfig": "tsconfig.server.json" + }, + "configurations": { + "production": { + "outputHashing": "media" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "sourceMap": true, + "extractLicenses": false, + "vendorChunk": true + } + }, + "defaultConfiguration": "production" + }, + "serve-ssr": { + "builder": "@angular-devkit/build-angular:ssr-dev-server", + "configurations": { + "development": { + "browserTarget": "angular:build:development", + "serverTarget": "angular:server:development" + }, + "production": { + "browserTarget": "angular:build:production", + "serverTarget": "angular:server:production" + } + }, + "defaultConfiguration": "development" + }, + "prerender": { + "builder": "@angular-devkit/build-angular:prerender", + "options": { + "routes": [ + "/" + ] + }, + "configurations": { + "production": { + "browserTarget": "angular:build:production", + "serverTarget": "angular:server:production" + }, + "development": { + "browserTarget": "angular:build:development", + "serverTarget": "angular:server:development" + } + }, + "defaultConfiguration": "production" + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/scripts/webframeworks-deploy-tests/angular/package-lock.json b/scripts/webframeworks-deploy-tests/angular/package-lock.json new file mode 100644 index 00000000000..0c7c66d43db --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/package-lock.json @@ -0,0 +1,13012 @@ +{ + "name": "angular", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "angular", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^17.0.5", + "@angular/common": "^17.0.5", + "@angular/compiler": "^17.0.5", + "@angular/core": "^17.0.5", + "@angular/forms": "^17.0.5", + "@angular/platform-browser": "^17.0.5", + "@angular/platform-browser-dynamic": "^17.0.5", + "@angular/platform-server": "^17.0.5", + "@angular/router": "^17.0.5", + "@angular/ssr": "^17.0.5", + "express": "^4.15.2", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.2" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.0.5", + "@angular/cli": "^17.0.5", + "@angular/compiler-cli": "^17.0.5", + "@angular/localize": "^17.0.5", + "@types/express": "^4.17.0", + "@types/jasmine": "~4.3.0", + "@types/node": "^14.15.0", + "jasmine-core": "~4.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "~5.2.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.5.tgz", + "integrity": "sha512-45+DTM3F8OFlMFRxQRgTBXnfndysgiZXiqItiKmFFau7wENZiTijUuFMFjOIHlLXFDI1qs130hYE4YkPNFffxg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.2.1", + "@angular-devkit/architect": "0.1700.5", + "@angular-devkit/build-webpack": "0.1700.5", + "@angular-devkit/core": "17.0.5", + "@babel/core": "7.23.2", + "@babel/generator": "7.23.0", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.22.6", + "@babel/plugin-transform-async-generator-functions": "7.23.2", + "@babel/plugin-transform-async-to-generator": "7.22.5", + "@babel/plugin-transform-runtime": "7.23.2", + "@babel/preset-env": "7.23.2", + "@babel/runtime": "7.23.2", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "17.0.5", + "@vitejs/plugin-basic-ssl": "1.0.1", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.16", + "babel-loader": "9.1.3", + "babel-plugin-istanbul": "6.1.1", + "browser-sync": "2.29.3", + "browserslist": "^4.21.5", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.20", + "css-loader": "6.8.1", + "esbuild-wasm": "0.19.5", + "fast-glob": "3.3.1", + "http-proxy-middleware": "2.0.6", + "https-proxy-agent": "7.0.2", + "inquirer": "9.2.11", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.5", + "mini-css-extract-plugin": "2.7.6", + "mrmime": "1.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "3.0.1", + "piscina": "4.1.0", + "postcss": "8.4.31", + "postcss-loader": "7.3.3", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.69.5", + "sass-loader": "13.3.2", + "semver": "7.5.4", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.24.0", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.6.2", + "undici": "5.27.2", + "vite": "4.5.0", + "webpack": "5.89.0", + "webpack-dev-middleware": "6.1.1", + "webpack-dev-server": "4.15.1", + "webpack-merge": "5.10.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.19.5" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "@angular/localize": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/service-worker": "^17.0.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^17.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.2 <5.3" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.1700.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.5.tgz", + "integrity": "sha512-kPGiPzystxyLDj79Wy+wCZs5vzx6iUy6fjZ9dKFNS3M9T9UXoo8CZLJS0dWrgO/97M25MSgufyIEDmi+HvwZ7w==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.0.5", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz", + "integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "3.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", + "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz", + "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz", + "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz", + "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz", + "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz", + "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz", + "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz", + "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz", + "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz", + "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz", + "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz", + "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz", + "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz", + "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz", + "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", + "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz", + "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz", + "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz", + "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz", + "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz", + "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz", + "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz", + "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.5", + "@esbuild/android-arm64": "0.19.5", + "@esbuild/android-x64": "0.19.5", + "@esbuild/darwin-arm64": "0.19.5", + "@esbuild/darwin-x64": "0.19.5", + "@esbuild/freebsd-arm64": "0.19.5", + "@esbuild/freebsd-x64": "0.19.5", + "@esbuild/linux-arm": "0.19.5", + "@esbuild/linux-arm64": "0.19.5", + "@esbuild/linux-ia32": "0.19.5", + "@esbuild/linux-loong64": "0.19.5", + "@esbuild/linux-mips64el": "0.19.5", + "@esbuild/linux-ppc64": "0.19.5", + "@esbuild/linux-riscv64": "0.19.5", + "@esbuild/linux-s390x": "0.19.5", + "@esbuild/linux-x64": "0.19.5", + "@esbuild/netbsd-x64": "0.19.5", + "@esbuild/openbsd-x64": "0.19.5", + "@esbuild/sunos-x64": "0.19.5", + "@esbuild/win32-arm64": "0.19.5", + "@esbuild/win32-ia32": "0.19.5", + "@esbuild/win32-x64": "0.19.5" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/piscina": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.1.0.tgz", + "integrity": "sha512-sjbLMi3sokkie+qmtZpkfMCUJTpbxJm/wvaPzU28vmYSsTSW8xk9JcFUsbqGJdtPpIQ9tuj+iDcTtgZjwnOSig==", + "dev": true, + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1700.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.5.tgz", + "integrity": "sha512-rLtDIK6je7JxhWG76aM8smfX13XHv+LlepwdK4lQqPEnz5BnkTfNFBnqwIWHA2eNUNTnVgeS356PxckZI3YL1g==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1700.5", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1700.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.5.tgz", + "integrity": "sha512-kPGiPzystxyLDj79Wy+wCZs5vzx6iUy6fjZ9dKFNS3M9T9UXoo8CZLJS0dWrgO/97M25MSgufyIEDmi+HvwZ7w==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.0.5", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz", + "integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "3.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.5.tgz", + "integrity": "sha512-KYPku0qTb8B+TtRbFqXGYpJOPg1k6d5bNHV6n8jTc35mlEUUghOd7HkovdfkQ3cgGNQM56a74D1CvSeruZEGsA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.0.5", + "jsonc-parser": "3.2.0", + "magic-string": "0.30.5", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz", + "integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "3.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular/animations": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.5.tgz", + "integrity": "sha512-NZ9Y3QWqrn0THypVNwsztMV9rnjxNMRIf6to8aZv+ehIUOvskqcA/lW5qAdcMr1uNoyloB9vahJrDniWWEKT5A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.0.5" + } + }, + "node_modules/@angular/cli": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.5.tgz", + "integrity": "sha512-IWtepjO1yTVGblbpTI7vtdxX5EjOYSL4BGa+3g85XuY6U2H38Bc9ZVBAYteAvRX1ZA2yvwJw068YY52ITlnr4A==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1700.5", + "@angular-devkit/core": "17.0.5", + "@angular-devkit/schematics": "17.0.5", + "@schematics/angular": "17.0.5", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.1", + "inquirer": "9.2.11", + "jsonc-parser": "3.2.0", + "npm-package-arg": "11.0.1", + "npm-pick-manifest": "9.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "17.0.4", + "resolve": "1.22.8", + "semver": "7.5.4", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1700.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.5.tgz", + "integrity": "sha512-kPGiPzystxyLDj79Wy+wCZs5vzx6iUy6fjZ9dKFNS3M9T9UXoo8CZLJS0dWrgO/97M25MSgufyIEDmi+HvwZ7w==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.0.5", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz", + "integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "3.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular/common": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.5.tgz", + "integrity": "sha512-1vFZ7nd8xyAYh/DwFtRuSieP8Dy/6QuOxl914/TOUr26F1a4e+7ywCyMLVjmYjx+WkZe7uu/Hgpr2raBaVTnQw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.0.5", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.5.tgz", + "integrity": "sha512-V6LnX/B2YXpzXeNWavtX/XPNUnWrVUFpiOniKqHYhAxXnibhyXL9DRsyVs8QbKgIcPPcQeJMHdAjklCWJsePvg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.0.5" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.5.tgz", + "integrity": "sha512-Nb99iKz8LMoc5HC9iu5rbWblXb68sHHI6bcN8sdqvc2g+PohkGNbtRjVZFhP+WKMaNFYDSvLWcHFFYItLRkT4g==", + "dev": true, + "dependencies": { + "@babel/core": "7.23.2", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.0.5", + "typescript": ">=5.2 <5.3" + } + }, + "node_modules/@angular/core": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.5.tgz", + "integrity": "sha512-siWUrdBWgTAqMnRF+qxGZznj5AdR/x3+8l0/bj4CkSZzwZGL/CHy40ec71bbgiPkYob1v4v40voXu2aSSeCLPg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, + "node_modules/@angular/forms": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.5.tgz", + "integrity": "sha512-d91Rre/NK+SgamF1OJmDJUx+Zs8M7qFmrKu7c+hNsXPe8J/fkMNoWFikne/WSsegwY929E1xpeqvu/KXQt90ug==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.0.5", + "@angular/core": "17.0.5", + "@angular/platform-browser": "17.0.5", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/localize": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.5.tgz", + "integrity": "sha512-9U/nNz490fX5ErDYUgaJIyHb39BvU00SkgfMIQG1O/QRxWZJxcoZ+8+fQFrZCsw/VIiH9sRIyYhM4AJsxC01hQ==", + "dev": true, + "dependencies": { + "@babel/core": "7.23.2", + "fast-glob": "3.3.1", + "yargs": "^17.2.1" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.0.5", + "@angular/compiler-cli": "17.0.5" + } + }, + "node_modules/@angular/platform-browser": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.5.tgz", + "integrity": "sha512-VJQ6bVS40xJLNGNcX59/QFPrZesIm2zETOqAc6K04onuWF1EnJqvcDog9eYJsm0sLWhQeCdWVmAFRenTkDoqng==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.0.5", + "@angular/common": "17.0.5", + "@angular/core": "17.0.5" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.5.tgz", + "integrity": "sha512-Ki+0B3/S+Rv3O4jf+tbDBPs0m+VUMoS6VVCCLviaurYGPLPtGblhCzRv49Zoyo5gEVoEOgnxS6CI91Tv6My9ug==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.0.5", + "@angular/compiler": "17.0.5", + "@angular/core": "17.0.5", + "@angular/platform-browser": "17.0.5" + } + }, + "node_modules/@angular/platform-server": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-17.0.5.tgz", + "integrity": "sha512-urnYha4tXg1Rzz0EAczwmLxW4ksWjgF0YCx0r3np47Hx2WP6O9OPjm5D5O/SoPcYUSxQvH9ntgysOtJWIVGmcQ==", + "dependencies": { + "tslib": "^2.3.0", + "xhr2": "^0.2.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.0.5", + "@angular/common": "17.0.5", + "@angular/compiler": "17.0.5", + "@angular/core": "17.0.5", + "@angular/platform-browser": "17.0.5" + } + }, + "node_modules/@angular/router": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.5.tgz", + "integrity": "sha512-9e5MQJzDdfhXKSYrduIDmDf73GBRcjx6qE+k5CliGY4sFza10wdbrM4LkiuA3Z2Ja+2AKkotrGG3ZMCtAsFY1g==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.0.5", + "@angular/core": "17.0.5", + "@angular/platform-browser": "17.0.5", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/ssr": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-17.0.5.tgz", + "integrity": "sha512-Snio+nw+Ur1p7utyZ68wK/0xajg7E+JZBZouA88L7U8f1++YQFJV80nAuRZNYQKIBN//IWoNW+xyM+FR15HQBA==", + "dependencies": { + "critters": "0.0.20", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.0", + "@angular/core": "^17.0.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.5.tgz", + "integrity": "sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", + "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", + "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", + "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", + "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.2.tgz", + "integrity": "sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", + "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.2", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.23.2", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.23.0", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.23.0", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.23.0", + "@babel/plugin-transform-modules-commonjs": "^7.23.0", + "@babel/plugin-transform-modules-systemjs": "^7.23.0", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.23.0", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.23.0", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz", + "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz", + "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz", + "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz", + "integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz", + "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz", + "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz", + "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz", + "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz", + "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz", + "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz", + "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz", + "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz", + "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz", + "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz", + "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz", + "integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz", + "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz", + "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz", + "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz", + "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz", + "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz", + "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@ljharb/through": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz", + "integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ngtools/webpack": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.5.tgz", + "integrity": "sha512-r82k7mxErJHtd6dzq0PKHQNhOuEjUZn95f2adJpO5mP/R/ms8LUk1ILvP3EocxkisYU8ET2EeGj3wQZC2g3RcA==", + "dev": true, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "typescript": ">=5.2 <5.3", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz", + "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.3.tgz", + "integrity": "sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", + "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz", + "integrity": "sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==", + "dev": true, + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.2.tgz", + "integrity": "sha512-Omu0rpA8WXvcGeY6DDzyRoY1i5DkCBkzyJ+m2u7PD6quzb0TvSqdIPOkTn8ZBOj7LbbcbMfZ3c5skwSu6m8y2w==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@schematics/angular": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.5.tgz", + "integrity": "sha512-sOc1UG4NiV+7cGwrbWPnyW71O+NgsKaFb2agSrVduRL7o4neMDeqF04ik4Kv1jKA7sZOQfPV+3cn6XI49Mumrw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.0.5", + "@angular-devkit/schematics": "17.0.5", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz", + "integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "3.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@sigstore/bundle": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz", + "integrity": "sha512-89uOo6yh/oxaU8AeOUnVrTdVMcGk9Q1hJa7Hkvalc6G3Z3CupWk4Xe9djSgJm9fMkH69s0P0cVHUoKSOemLdng==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz", + "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.0.tgz", + "integrity": "sha512-AAbmnEHDQv6CSfrWA5wXslGtzLPtAtHZleKOgxdQYvx/s76Fk6T6ZVt7w2IGV9j1UrFeBocTTQxaXG2oRrDhYA==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.1.0", + "@sigstore/protobuf-specs": "^0.2.1", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.2.0.tgz", + "integrity": "sha512-KKATZ5orWfqd9ZG6MN8PtCIx4eevWSuGRKQvofnWXRpyMyUEpmrzg5M5BrCpjM+NfZ0RbNGOh5tCz/P2uoRqOA==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.1", + "tuf-js": "^2.1.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz", + "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.8", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz", + "integrity": "sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "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==", + "dev": true, + "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.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": 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==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "node_modules/@types/node-forge": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", + "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", + "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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==" + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", + "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", + "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.3", + "core-js-compat": "^3.33.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", + "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": 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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/bonjour-service/node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.29.3.tgz", + "integrity": "sha512-NiM38O6XU84+MN+gzspVmXV2fTOoe+jBqIBx3IBdhZrdeURr6ZgznJr/p+hQ+KzkKEiGH/GcC4SQFSL0jV49bg==", + "dev": true, + "dependencies": { + "browser-sync-client": "^2.29.3", + "browser-sync-ui": "^2.29.3", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.0.1", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "localtunnel": "^2.0.1", + "micromatch": "^4.0.2", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "0.16.2", + "serve-index": "1.9.1", + "serve-static": "1.13.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" + }, + "bin": { + "browser-sync": "dist/bin.js" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/browser-sync-client": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.29.3.tgz", + "integrity": "sha512-4tK5JKCl7v/3aLbmCBMzpufiYLsB1+UI+7tUXCCp5qF0AllHy/jAqYu6k7hUF3hYtlClKpxExWaR+rH+ny07wQ==", + "dev": true, + "dependencies": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/browser-sync-ui": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.29.3.tgz", + "integrity": "sha512-kBYOIQjU/D/3kYtUIJtj82e797Egk1FB2broqItkr3i4eF1qiHbFCG6srksu9gWhfmuM/TNG76jMfzAdxEPakg==", + "dev": true, + "dependencies": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" + } + }, + "node_modules/browser-sync-ui/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync-ui/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync-ui/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==", + "dev": true + }, + "node_modules/browser-sync-ui/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync-ui/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync/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==", + "dev": true + }, + "node_modules/browser-sync/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "dev": true + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.1.tgz", + "integrity": "sha512-g4Uf2CFZPaxtJKre6qr4zqLDOOPU7bNVhWjlNhvzc51xaTOx2noMOLhfFkTAqwtrAZAKQUuDfyjitzilpA8WsQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "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==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "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==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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==" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.34.0.tgz", + "integrity": "sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==", + "dev": true, + "dependencies": { + "browserslist": "^4.22.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/critters": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.20.tgz", + "integrity": "sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw==", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/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==" + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "dev": true, + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dev": true, + "dependencies": { + "lodash": "^4.17.10" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dev": true, + "dependencies": { + "chalk": "4.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eazy-logger/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eazy-logger/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eazy-logger/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==", + "dev": true + }, + "node_modules/eazy-logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eazy-logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.604", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.604.tgz", + "integrity": "sha512-JAJ4lyLJYudlgJPYJicimU9R+qZ/3iyeyQS99bfT7PWi7psYWeN84lPswTjpHxQueU34PKxM/IJzQS6poYlovQ==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz", + "integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.17", + "@esbuild/android-arm64": "0.18.17", + "@esbuild/android-x64": "0.18.17", + "@esbuild/darwin-arm64": "0.18.17", + "@esbuild/darwin-x64": "0.18.17", + "@esbuild/freebsd-arm64": "0.18.17", + "@esbuild/freebsd-x64": "0.18.17", + "@esbuild/linux-arm": "0.18.17", + "@esbuild/linux-arm64": "0.18.17", + "@esbuild/linux-ia32": "0.18.17", + "@esbuild/linux-loong64": "0.18.17", + "@esbuild/linux-mips64el": "0.18.17", + "@esbuild/linux-ppc64": "0.18.17", + "@esbuild/linux-riscv64": "0.18.17", + "@esbuild/linux-s390x": "0.18.17", + "@esbuild/linux-x64": "0.18.17", + "@esbuild/netbsd-x64": "0.18.17", + "@esbuild/openbsd-x64": "0.18.17", + "@esbuild/sunos-x64": "0.18.17", + "@esbuild/win32-arm64": "0.18.17", + "@esbuild/win32-ia32": "0.18.17", + "@esbuild/win32-x64": "0.18.17" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.5.tgz", + "integrity": "sha512-7zmLLn2QCj93XfMmHtzrDJ1UBuOHB2CZz1ghoCEZiRajxjUvHsF40PnbzFIY/pmesqPRaEtEWii0uzsTbnAgrA==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": 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==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/express/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/express/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==" + }, + "node_modules/express/node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/figures": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", + "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0", + "is-unicode-supported": "^1.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": 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==", + "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==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", + "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.11.tgz", + "integrity": "sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==", + "dev": true, + "dependencies": { + "@ljharb/through": "^2.3.9", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^5.0.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/inquirer/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/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==", + "dev": true + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "dev": true, + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", + "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/karma/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/karma/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/karma/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/karma/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/karma/node_modules/ua-parser-js": { + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/localtunnel": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz", + "integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==", + "dev": true, + "dependencies": { + "axios": "0.21.4", + "debug": "4.3.2", + "openurl": "1.1.1", + "yargs": "17.1.1" + }, + "bin": { + "lt": "bin/lt.js" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/localtunnel/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/localtunnel/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/localtunnel/node_modules/yargs": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz", + "integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/localtunnel/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/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==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", + "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", + "dev": true, + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", + "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz", + "integrity": "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.1.tgz", + "integrity": "sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/nopt": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", + "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", + "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.0.tgz", + "integrity": "sha512-ErAGFB5kJUciPy1mmx/C2YFbvxoJ0QJ9uwkCZOeR6CqLLISPZBOiFModAbSXnjjlwW5lOhuhXva+fURsSGJqyw==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.1.0.tgz", + "integrity": "sha512-PQCELXKt8Azvxnt5Y85GseQDJJlglTFM9L9U9gkv2y4e9s0k3GVDdOx3YoB6gm2Do0hlkzC39iCGXby+Wve1Bw==", + "dev": true, + "dependencies": { + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openurl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", + "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", + "dev": true + }, + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/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==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.4.tgz", + "integrity": "sha512-eGdLHrV/g5b5MtD5cTPyss+JxOlaOloSMG3UwPMAvL8ywaLJ6beONPF40K4KKl/UI6q5hTKCJq5rCu8tkF+7Dg==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^7.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^2.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "dev": true, + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", + "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.2.0", + "jiti": "^1.18.2", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "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==", + "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==", + "dev": true, + "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/read-package-json": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.0.tgz", + "integrity": "sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/resp-modifier/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/resp-modifier/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "dev": true + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", + "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "dev": true + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "dev": true, + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/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==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "dev": true + }, + "node_modules/send/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true, + "bin": { + "mime": "cli.js" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/send/node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "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==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sigstore": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.1.0.tgz", + "integrity": "sha512-kPIj+ZLkyI3QaM0qX8V/nSsweYND3W448pwkDgS6CQ74MfhEkIR8ToK5Iyx46KJYRjseVcD3Rp9zAmUAj6ZjPw==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.1.0", + "@sigstore/protobuf-specs": "^0.2.1", + "@sigstore/sign": "^2.1.0", + "@sigstore/tuf": "^2.1.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", + "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-throttle": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "dev": true, + "dependencies": { + "commander": "^2.2.0", + "limiter": "^1.0.5" + }, + "bin": { + "throttleproxy": "bin/throttleproxy.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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==", + "dev": 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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "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==", + "dev": 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==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tuf-js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.1.0.tgz", + "integrity": "sha512-eD7YPPjVlMzdggrOeE8zwoegUaG/rt6Bt3jwoQPunRiNVzgcCE009UDFJKJjG+Gk9wFu6W/Vi+P5d/5QpdD9jA==", + "dev": true, + "dependencies": { + "@tufjs/models": "2.0.0", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "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==", + "dev": 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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "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==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz", + "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "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==", + "dev": 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/wrap-ansi-cjs": { + "name": "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==", + "dev": 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/wrap-ansi-cjs/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true + }, + "node_modules/wrap-ansi/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": 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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.2.tgz", + "integrity": "sha512-X4U7J1isDhoOmHmFWiLhloWc2lzMkdnumtfQ1LXzf/IOZp5NQYuMUTaviVzG/q1ugMBIXzin2AqeVJUoSEkNyQ==", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/scripts/webframeworks-deploy-tests/angular/package.json b/scripts/webframeworks-deploy-tests/angular/package.json new file mode 100644 index 00000000000..9de1978ed19 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/package.json @@ -0,0 +1,48 @@ +{ + "name": "angular", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "dev:ssr": "ng run angular:serve-ssr", + "serve:ssr": "node dist/angular/server/en/main.js", + "build:ssr": "ng build && ng run angular:server", + "prerender": "ng run angular:prerender" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.0.5", + "@angular/common": "^17.0.5", + "@angular/compiler": "^17.0.5", + "@angular/core": "^17.0.5", + "@angular/forms": "^17.0.5", + "@angular/platform-browser": "^17.0.5", + "@angular/platform-browser-dynamic": "^17.0.5", + "@angular/platform-server": "^17.0.5", + "@angular/router": "^17.0.5", + "@angular/ssr": "^17.0.5", + "express": "^4.15.2", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.2" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.0.5", + "@angular/cli": "^17.0.5", + "@angular/compiler-cli": "^17.0.5", + "@angular/localize": "^17.0.5", + "@types/express": "^4.17.0", + "@types/jasmine": "~4.3.0", + "@types/node": "^14.15.0", + "jasmine-core": "~4.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "~5.2.2" + } +} \ No newline at end of file diff --git a/scripts/webframeworks-deploy-tests/angular/server.ts b/scripts/webframeworks-deploy-tests/angular/server.ts new file mode 100644 index 00000000000..c4679e648d9 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/server.ts @@ -0,0 +1,73 @@ + +import 'zone.js/node'; + +import { APP_BASE_HREF } from '@angular/common'; +import { CommonEngine } from '@angular/ssr'; +import * as express from 'express'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import bootstrap from './src/main.server'; +import { LOCALE_ID } from '@angular/core'; + +// The Express app is exported so that it can be used by serverless Functions. +export function app(locale: string): express.Express { + const server = express(); + const distFolder = join(process.cwd(), `dist/angular/browser/${locale}`); + const indexHtml = existsSync(join(distFolder, 'index.original.html')) + ? join(distFolder, 'index.original.html') + : join(distFolder, 'index.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', distFolder); + + // Example Express Rest API endpoints + // server.get('/api/**', (req, res) => { }); + // Serve static files from /browser + server.get('*.*', express.static(distFolder, { + maxAge: '1y' + })); + + // All regular routes use the Angular engine + server.get('*', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: distFolder, + providers: [ + { provide: APP_BASE_HREF, useValue: baseUrl }, + { provide: LOCALE_ID, useValue: locale },], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; +} + +function run(): void { + const port = process.env['PORT'] || 4000; + + // Start up the Node server + const server = app('en'); + server.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +// Webpack will replace 'require' with '__webpack_require__' +// '__non_webpack_require__' is a proxy to Node 'require' +// The below code is to ensure that the server is run only when not requiring the bundle. +declare const __non_webpack_require__: NodeRequire; +const mainModule = __non_webpack_require__.main; +const moduleFilename = mainModule && mainModule.filename || ''; +if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { + run(); +} + +export default bootstrap; diff --git a/scripts/webframeworks-deploy-tests/angular/src/app/app-routing.module.ts b/scripts/webframeworks-deploy-tests/angular/src/app/app-routing.module.ts new file mode 100644 index 00000000000..1d675d4af27 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/app/app-routing.module.ts @@ -0,0 +1,8 @@ +import { Routes } from '@angular/router'; +import { FooComponent } from './foo/foo.component'; +import { HomeComponent } from './home/home.component'; + +export const routes: Routes = [ + { path: '', component: HomeComponent }, + { path: 'foo/:id', component: FooComponent } +]; diff --git a/scripts/webframeworks-deploy-tests/angular/src/app/app.component.ts b/scripts/webframeworks-deploy-tests/angular/src/app/app.component.ts new file mode 100644 index 00000000000..643e5d7e009 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/app/app.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [CommonModule, RouterOutlet], + template: ` + + `, + styles: [] +}) +export class AppComponent { +} diff --git a/scripts/webframeworks-deploy-tests/angular/src/app/app.config.server.ts b/scripts/webframeworks-deploy-tests/angular/src/app/app.config.server.ts new file mode 100644 index 00000000000..b4d57c94235 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/app/app.config.server.ts @@ -0,0 +1,11 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/platform-server'; +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering() + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/scripts/webframeworks-deploy-tests/angular/src/app/app.config.ts b/scripts/webframeworks-deploy-tests/angular/src/app/app.config.ts new file mode 100644 index 00000000000..ca2bef27b82 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/app/app.config.ts @@ -0,0 +1,9 @@ +import { ApplicationConfig, NgModule } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { routes } from './app-routing.module'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + ], +} diff --git a/scripts/webframeworks-deploy-tests/angular/src/app/foo/foo.component.ts b/scripts/webframeworks-deploy-tests/angular/src/app/foo/foo.component.ts new file mode 100644 index 00000000000..081f11f98c3 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/app/foo/foo.component.ts @@ -0,0 +1,11 @@ +import { Component, Inject } from '@angular/core'; +import { LOCALE_ID } from '@angular/core'; + +@Component({ + selector: 'app-foo', + template: `Foo {{ locale }}`, + styles: [] +}) +export class FooComponent { + constructor(@Inject(LOCALE_ID) protected locale: string) {} +} diff --git a/scripts/webframeworks-deploy-tests/angular/src/app/home/home.component.ts b/scripts/webframeworks-deploy-tests/angular/src/app/home/home.component.ts new file mode 100644 index 00000000000..d08dc37ad0b --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/app/home/home.component.ts @@ -0,0 +1,11 @@ +import { Component, Inject } from '@angular/core'; +import { LOCALE_ID } from '@angular/core'; + +@Component({ + selector: 'app-home', + template: `Home {{ locale }}`, + styles: [] +}) +export class HomeComponent { + constructor(@Inject(LOCALE_ID) protected locale: string) {} +} diff --git a/scripts/webframeworks-deploy-tests/angular/src/assets/.gitkeep b/scripts/webframeworks-deploy-tests/angular/src/assets/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/webframeworks-deploy-tests/angular/src/favicon.ico b/scripts/webframeworks-deploy-tests/angular/src/favicon.ico new file mode 100644 index 00000000000..1cceb832013 Binary files /dev/null and b/scripts/webframeworks-deploy-tests/angular/src/favicon.ico differ diff --git a/scripts/webframeworks-deploy-tests/angular/src/index.html b/scripts/webframeworks-deploy-tests/angular/src/index.html new file mode 100644 index 00000000000..cd38374ece5 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/scripts/webframeworks-deploy-tests/angular/src/locale/messages.es.xlf b/scripts/webframeworks-deploy-tests/angular/src/locale/messages.es.xlf new file mode 100644 index 00000000000..b0c2d11206f --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/locale/messages.es.xlf @@ -0,0 +1,7 @@ + + + + + + + diff --git a/scripts/webframeworks-deploy-tests/angular/src/locale/messages.fr.xlf b/scripts/webframeworks-deploy-tests/angular/src/locale/messages.fr.xlf new file mode 100644 index 00000000000..b0c2d11206f --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/locale/messages.fr.xlf @@ -0,0 +1,7 @@ + + + + + + + diff --git a/scripts/webframeworks-deploy-tests/angular/src/main.server.ts b/scripts/webframeworks-deploy-tests/angular/src/main.server.ts new file mode 100644 index 00000000000..4b9d4d1545c --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/main.server.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; diff --git a/scripts/webframeworks-deploy-tests/angular/src/main.ts b/scripts/webframeworks-deploy-tests/angular/src/main.ts new file mode 100644 index 00000000000..d18fe370bff --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/main.ts @@ -0,0 +1,8 @@ +/// +import { appConfig } from './app/app.config'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; + + +bootstrapApplication(AppComponent, appConfig) + .catch(err => console.error(err)); diff --git a/scripts/webframeworks-deploy-tests/angular/src/styles.css b/scripts/webframeworks-deploy-tests/angular/src/styles.css new file mode 100644 index 00000000000..90d4ee0072c --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/scripts/webframeworks-deploy-tests/angular/tsconfig.app.json b/scripts/webframeworks-deploy-tests/angular/tsconfig.app.json new file mode 100644 index 00000000000..ec26f7034c7 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/tsconfig.app.json @@ -0,0 +1,16 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + "@angular/localize" + ] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/scripts/webframeworks-deploy-tests/angular/tsconfig.json b/scripts/webframeworks-deploy-tests/angular/tsconfig.json new file mode 100644 index 00000000000..ed966d43afa --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/scripts/webframeworks-deploy-tests/angular/tsconfig.server.json b/scripts/webframeworks-deploy-tests/angular/tsconfig.server.json new file mode 100644 index 00000000000..a79755d9d08 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/tsconfig.server.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "./out-tsc/server", + "types": [ + "node", + "@angular/localize" + ] + }, + "files": [ + "src/main.server.ts", + "server.ts" + ] +} diff --git a/scripts/webframeworks-deploy-tests/angular/tsconfig.spec.json b/scripts/webframeworks-deploy-tests/angular/tsconfig.spec.json new file mode 100644 index 00000000000..c63b6982a65 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/angular/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine", + "@angular/localize" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/scripts/webframeworks-deploy-tests/firebase.json b/scripts/webframeworks-deploy-tests/firebase.json new file mode 100644 index 00000000000..a59a94f84a8 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/firebase.json @@ -0,0 +1,40 @@ +{ + "hosting": [ + { + "target": "nextjs", + "source": "nextjs", + "frameworksBackend": { + "maxInstances": 1, + "region": "asia-east1" + }, + "rewrites": [{ + "source": "helloWorld", + "function": "helloWorld" + }] + }, + { + "target": "angular", + "source": "angular", + "frameworksBackend": { + "maxInstances": 1, + "region": "europe-west1" + }, + "rewrites": [{ + "source": "helloWorld", + "function": "helloWorld" + }] + } + ], + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log" + ] + } + ] +} diff --git a/scripts/webframeworks-deploy-tests/functions/.gitignore b/scripts/webframeworks-deploy-tests/functions/.gitignore new file mode 100644 index 00000000000..40b878db5b1 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/functions/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/scripts/webframeworks-deploy-tests/functions/index.js b/scripts/webframeworks-deploy-tests/functions/index.js new file mode 100644 index 00000000000..ebf396726e7 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/functions/index.js @@ -0,0 +1,5 @@ +import { onRequest } from "firebase-functions/v2/https"; + +export const helloWorld = onRequest((request, response) => { + response.send("Hello from Firebase!"); +}); diff --git a/scripts/webframeworks-deploy-tests/functions/package-lock.json b/scripts/webframeworks-deploy-tests/functions/package-lock.json new file mode 100644 index 00000000000..7af38d5ce34 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/functions/package-lock.json @@ -0,0 +1,6401 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^11.11.0", + "firebase-functions": "^4.5.0" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "peer": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "dev": true, + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/generator": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.23.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "peer": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "peer": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "devOptional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/types": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "peer": true + }, + "node_modules/@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz", + "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.7", + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "fast-xml-parser": "^4.2.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.21", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.21.tgz", + "integrity": "sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "peer": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true, + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.7.tgz", + "integrity": "sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.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==" + }, + "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==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "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==", + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "peer": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.7", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", + "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.4", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", + "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "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==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "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==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "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==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "optional": true + }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, + "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==", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "optional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "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==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "node_modules/@types/node": { + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" + }, + "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==" + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "peer": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "peer": 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==", + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": 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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "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==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": 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" + } + ], + "optional": true + }, + "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==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "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.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "peer": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true, + "peer": true + }, + "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==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "peer": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "peer": true + }, + "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==", + "devOptional": 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==", + "devOptional": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true + }, + "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==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "peer": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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==" + }, + "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==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "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==", + "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==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.607", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.607.tgz", + "integrity": "sha512-YUlnPwE6eYxzwBnFmawA8LiLRfm70R2aJRIUv0n03uHt/cUzzYACOogmvk8M2+hVzt/kB80KJXx7d5f5JofPvQ==", + "dev": true, + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "devOptional": true + }, + "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==", + "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==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": 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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "optional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "optional": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "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==", + "optional": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "optional": true + }, + "node_modules/fast-xml-parser": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "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==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-admin": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "dependencies": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^0.3.4", + "@firebase/database-types": "^0.10.4", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.8.0", + "@google-cloud/storage": "^6.9.5" + } + }, + "node_modules/firebase-functions": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.5.0.tgz", + "integrity": "sha512-y6HsasHtGLfXCp3Pfrz+JA19lO9hSzYiNxFDIDMffrfcsG7UbXzv0zfi2ASadMVRoDCaox5ppZBa1QJxZbctPQ==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.1.0.tgz", + "integrity": "sha512-yfm9ToguShxmRXb7TINN88zE2bM9gsBbs7vMWVKJAxGcl/n1f/U0sT5k2yho676QIcSqXVSjCONU8W4cUEL+Sw==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "firebase-functions": ">=4.3.0", + "jest": ">=28.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "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==", + "optional": true + }, + "node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "optional": true, + "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/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "optional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "peer": 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==", + "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==" + }, + "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==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "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==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "peer": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "peer": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "peer": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "peer": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "peer": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "peer": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "peer": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "peer": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "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==", + "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==", + "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==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "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==", + "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/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "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==", + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", + "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "peer": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.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==", + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "peer": true + }, + "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==", + "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==", + "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==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true, + "peer": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "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==", + "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==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "peer": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto3-json-serializer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz", + "integrity": "sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==", + "optional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "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/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/protobufjs-cli/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/protobufjs-cli/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs-cli/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "peer": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, + "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==", + "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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "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==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "peer": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "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==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "peer": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "devOptional": 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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", + "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "optional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "peer": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "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==" + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "optional": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "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" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "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==" + }, + "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==", + "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==", + "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==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true, + "engines": { + "node": ">=0.10.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==", + "devOptional": 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==", + "devOptional": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "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==", + "devOptional": 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==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": 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==", + "devOptional": 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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/scripts/webframeworks-deploy-tests/functions/package.json b/scripts/webframeworks-deploy-tests/functions/package.json new file mode 100644 index 00000000000..35c1a474e74 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/functions/package.json @@ -0,0 +1,24 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "type": "module", + "scripts": { + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "20" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^11.11.0", + "firebase-functions": "^4.5.0" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0" + }, + "private": true +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/.eslintrc.json b/scripts/webframeworks-deploy-tests/nextjs/.eslintrc.json new file mode 100644 index 00000000000..2c63c085104 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/.eslintrc.json @@ -0,0 +1,2 @@ +{ +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/.gitignore b/scripts/webframeworks-deploy-tests/nextjs/.gitignore new file mode 100644 index 00000000000..4f360c89d2a --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.vscode diff --git a/scripts/webframeworks-deploy-tests/nextjs/app/app/api/dynamic/route.ts b/scripts/webframeworks-deploy-tests/nextjs/app/app/api/dynamic/route.ts new file mode 100644 index 00000000000..ba4529dcd0e --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/app/api/dynamic/route.ts @@ -0,0 +1,13 @@ +import { headers } from 'next/headers'; + +export async function GET() { + const _ = headers(); + return new Response(JSON.stringify([1, 2, 3]), { + status: 200, + headers: { + "content-type": "application/json", + "custom-header": "custom-value-2", + }, + }); + } + \ No newline at end of file diff --git a/scripts/webframeworks-deploy-tests/nextjs/app/app/api/static/route.ts b/scripts/webframeworks-deploy-tests/nextjs/app/app/api/static/route.ts new file mode 100644 index 00000000000..2028eeab7f5 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/app/api/static/route.ts @@ -0,0 +1,9 @@ +export async function GET() { + return new Response(JSON.stringify([1, 2, 3]), { + status: 200, + headers: { + "content-type": "application/json", + "custom-header": "custom-value", + }, + }); +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/app/app/image/page.tsx b/scripts/webframeworks-deploy-tests/nextjs/app/app/image/page.tsx new file mode 100644 index 00000000000..efe619104da --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/app/image/page.tsx @@ -0,0 +1,10 @@ +import Image from 'next/image' + +export default function PageWithImage() { + return ; +} \ No newline at end of file diff --git a/scripts/webframeworks-deploy-tests/nextjs/app/app/isr/page.tsx b/scripts/webframeworks-deploy-tests/nextjs/app/app/isr/page.tsx new file mode 100644 index 00000000000..cf8603cd7d9 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/app/isr/page.tsx @@ -0,0 +1,5 @@ +export const revalidate = 60; + +export default function ISR() { + return <>ISR; +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/app/app/ssg/page.tsx b/scripts/webframeworks-deploy-tests/nextjs/app/app/ssg/page.tsx new file mode 100644 index 00000000000..a5587ba760e --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/app/ssg/page.tsx @@ -0,0 +1,3 @@ +export default function SSG() { + return <>SSG; +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/app/app/ssr/page.tsx b/scripts/webframeworks-deploy-tests/nextjs/app/app/ssr/page.tsx new file mode 100644 index 00000000000..1a6d118e19e --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/app/ssr/page.tsx @@ -0,0 +1,8 @@ +'use server' + +import { headers } from 'next/headers'; + +export default async function SSR() { + const headersList = headers(); + return <>SSR; +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/app/layout.tsx b/scripts/webframeworks-deploy-tests/nextjs/app/layout.tsx new file mode 100644 index 00000000000..7b221173feb --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/layout.tsx @@ -0,0 +1,8 @@ +export default function RootLayout({ children }: any) { + return ( + + + {children} + + ) +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/middleware.ts b/scripts/webframeworks-deploy-tests/nextjs/middleware.ts new file mode 100644 index 00000000000..114232407cc --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/middleware.ts @@ -0,0 +1,12 @@ +// middleware.ts +import { NextRequest, NextResponse } from 'next/server'; + +// This function can be marked `async` if using `await` inside +export function middleware(request: NextRequest) { + return NextResponse.redirect(new URL('/about-2', request.url)); +} + +// See "Matching Paths" below to learn more +export const config = { + matcher: '/about/:path*', +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/next.config.js b/scripts/webframeworks-deploy-tests/nextjs/next.config.js new file mode 100644 index 00000000000..11d04a8c270 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/next.config.js @@ -0,0 +1,40 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + swcMinify: true, + experimental: { + serverActions: true, + }, + basePath: "/base", + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + }, + images: { + domains: ['google.com'], + }, + rewrites: () => [{ + source: '/about', + destination: '/', + },], + redirects: () => [{ + source: '/about', + destination: '/', + permanent: true, + },], + headers: () => [{ + source: '/about', + headers: [ + { + key: 'x-custom-header', + value: 'my custom header value', + }, + { + key: 'x-another-custom-header', + value: 'my other custom header value', + }, + ], + },], +} + +module.exports = nextConfig diff --git a/scripts/webframeworks-deploy-tests/nextjs/package-lock.json b/scripts/webframeworks-deploy-tests/nextjs/package-lock.json new file mode 100644 index 00000000000..f7a6b5c0c70 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/package-lock.json @@ -0,0 +1,7037 @@ +{ + "name": "hosting", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "hosting", + "version": "0.1.0", + "dependencies": { + "next": "14.0.3", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.0.3", + "typescript": "^5" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@next/env": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz", + "integrity": "sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.3.tgz", + "integrity": "sha512-j4K0n+DcmQYCVnSAM+UByTVfIHnYQy2ODozfQP+4RdwtRDfobrIvKq1K4Exb2koJ79HSSa7s6B2SA8T/1YR3RA==", + "dev": true, + "dependencies": { + "glob": "7.1.7" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz", + "integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz", + "integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz", + "integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz", + "integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz", + "integrity": "sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz", + "integrity": "sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz", + "integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz", + "integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz", + "integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.0.tgz", + "integrity": "sha512-2OCURAmRtdlL8iUDTypMrrxfwe8frXTeXaxGsVOaYtc/wrUyk8Z/0OBetM7cdlsy7ZFWlMX72VogKeh+A4Xcjw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.2.12", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz", + "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==", + "dev": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.42", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.42.tgz", + "integrity": "sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", + "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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==", + "dev": 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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001416", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz", + "integrity": "sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "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==", + "dev": 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==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", + "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.3.tgz", + "integrity": "sha512-IKPhpLdpSUyKofmsXUfrvBC49JMUTdeaD8ZIH4v9Vk0sC1X6URTuTJCLtA0Vwuj7V/CQh0oISuSTvNn5//Buew==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "14.0.3", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz", + "integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "get-tsconfig": "^4.5.0", + "globby": "^13.1.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/globby": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", + "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "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==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.5.0.tgz", + "integrity": "sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/next": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz", + "integrity": "sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==", + "dependencies": { + "@next/env": "14.0.3", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.31", + "styled-jsx": "5.1.1", + "watchpack": "2.4.0" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.0.3", + "@next/swc-darwin-x64": "14.0.3", + "@next/swc-linux-arm64-gnu": "14.0.3", + "@next/swc-linux-arm64-musl": "14.0.3", + "@next/swc-linux-x64-gnu": "14.0.3", + "@next/swc-linux-x64-musl": "14.0.3", + "@next/swc-win32-arm64-msvc": "14.0.3", + "@next/swc-win32-ia32-msvc": "14.0.3", + "@next/swc-win32-x64-msvc": "14.0.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "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==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "@next/env": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz", + "integrity": "sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA==" + }, + "@next/eslint-plugin-next": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.3.tgz", + "integrity": "sha512-j4K0n+DcmQYCVnSAM+UByTVfIHnYQy2ODozfQP+4RdwtRDfobrIvKq1K4Exb2koJ79HSSa7s6B2SA8T/1YR3RA==", + "dev": true, + "requires": { + "glob": "7.1.7" + } + }, + "@next/swc-darwin-arm64": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz", + "integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz", + "integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz", + "integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz", + "integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==", + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz", + "integrity": "sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==", + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz", + "integrity": "sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz", + "integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==", + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz", + "integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz", + "integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==", + "optional": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pkgr/utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.0.tgz", + "integrity": "sha512-2OCURAmRtdlL8iUDTypMrrxfwe8frXTeXaxGsVOaYtc/wrUyk8Z/0OBetM7cdlsy7ZFWlMX72VogKeh+A4Xcjw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.2.12", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.5.0" + } + }, + "@rushstack/eslint-patch": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz", + "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==", + "dev": true + }, + "@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/node": { + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "18.2.42", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.42.tgz", + "integrity": "sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "@typescript-eslint/parser": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", + "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + } + }, + "@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "requires": { + "dequal": "^2.0.3" + } + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, + "ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true + }, + "axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "requires": { + "dequal": "^2.0.3" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true + }, + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "requires": { + "big-integer": "^1.6.44" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "requires": { + "run-applescript": "^5.0.0" + } + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001416", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz", + "integrity": "sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "requires": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + } + }, + "default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "requires": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + } + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", + "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + } + }, + "es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "requires": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + } + }, + "eslint-config-next": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.3.tgz", + "integrity": "sha512-IKPhpLdpSUyKofmsXUfrvBC49JMUTdeaD8ZIH4v9Vk0sC1X6URTuTJCLtA0Vwuj7V/CQh0oISuSTvNn5//Buew==", + "dev": true, + "requires": { + "@next/eslint-plugin-next": "14.0.3", + "@rushstack/eslint-patch": "^1.3.3", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-import-resolver-typescript": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz", + "integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "get-tsconfig": "^4.5.0", + "globby": "^13.1.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "synckit": "^0.8.5" + }, + "dependencies": { + "globby": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", + "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "dev": true, + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, + "requires": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + } + }, + "eslint-plugin-react": { + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "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==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "get-tsconfig": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.5.0.tgz", + "integrity": "sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==", + "dev": true + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + } + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + }, + "dependencies": { + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + } + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "requires": { + "language-subtag-registry": "^0.3.20" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "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==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "next": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz", + "integrity": "sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==", + "requires": { + "@next/env": "14.0.3", + "@next/swc-darwin-arm64": "14.0.3", + "@next/swc-darwin-x64": "14.0.3", + "@next/swc-linux-arm64-gnu": "14.0.3", + "@next/swc-linux-arm64-musl": "14.0.3", + "@next/swc-linux-x64-gnu": "14.0.3", + "@next/swc-linux-x64-musl": "14.0.3", + "@next/swc-win32-arm64-msvc": "14.0.3", + "@next/swc-win32-ia32-msvc": "14.0.3", + "@next/swc-win32-x64-msvc": "14.0.3", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.31", + "styled-jsx": "5.1.1", + "watchpack": "2.4.0" + } + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } + } + }, + "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==", + "dev": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dev": true, + "requires": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "requires": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + } + }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + }, + "dependencies": { + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + } + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, + "string.prototype.matchall": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "requires": { + "client-only": "0.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "requires": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "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==", + "dev": true + } + } +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/package.json b/scripts/webframeworks-deploy-tests/nextjs/package.json new file mode 100644 index 00000000000..284f8bbe221 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/package.json @@ -0,0 +1,24 @@ +{ + "name": "hosting", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "react": "^18", + "react-dom": "^18", + "next": "14.0.3" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.0.3" + } +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/pages/_app.tsx b/scripts/webframeworks-deploy-tests/nextjs/pages/_app.tsx new file mode 100644 index 00000000000..3f5c9d54858 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/pages/_app.tsx @@ -0,0 +1,8 @@ +import '../styles/globals.css' +import type { AppProps } from 'next/app' + +function MyApp({ Component, pageProps }: AppProps) { + return +} + +export default MyApp diff --git a/scripts/webframeworks-deploy-tests/nextjs/pages/api/hello.ts b/scripts/webframeworks-deploy-tests/nextjs/pages/api/hello.ts new file mode 100644 index 00000000000..f8bcc7e5cae --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/pages/api/hello.ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/pages/index.tsx b/scripts/webframeworks-deploy-tests/nextjs/pages/index.tsx new file mode 100644 index 00000000000..86b5b3b5bf3 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/pages/index.tsx @@ -0,0 +1,72 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import Image from 'next/image' +import styles from '../styles/Home.module.css' + +const Home: NextPage = () => { + return ( + + ) +} + +export default Home diff --git a/scripts/webframeworks-deploy-tests/nextjs/pages/pages/fallback/[id].tsx b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/fallback/[id].tsx new file mode 100644 index 00000000000..ebe3c801ec7 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/fallback/[id].tsx @@ -0,0 +1,22 @@ +import { useRouter } from "next/router"; + +export const getStaticPaths = async () => { + return { + paths: [ + { params: { id: '1' }, locale: 'en' }, + { params: { id: '2' }, locale: 'en' }, + { params: { id: '1' }, locale: 'fr' }, + { params: { id: '2' }, locale: 'fr' }, + ], + fallback: true, + }; +} + +export const getStaticProps = async () => { + return { props: { } }; +} + +export default function SSG() { + const { locale, query: { id }} = useRouter(); + return <>SSG {id} {locale}; +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/pages/pages/isr/index.tsx b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/isr/index.tsx new file mode 100644 index 00000000000..27d350a3700 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/isr/index.tsx @@ -0,0 +1,10 @@ +import { useRouter } from "next/router"; + +export const getStaticProps = async () => { + return { props: { }, revalidate: 10 }; +} + +export default function ISR() { + const { locale } = useRouter(); + return <>ISR { locale }; +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/pages/pages/ssg/index.tsx b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/ssg/index.tsx new file mode 100644 index 00000000000..5d7262584a0 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/ssg/index.tsx @@ -0,0 +1,10 @@ +import { useRouter } from "next/router"; + +export const getStaticProps = async () => { + return { props: { } }; +} + +export default function SSG() { + const { locale } = useRouter(); + return <>SSG { locale }; +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/pages/pages/ssr/index.tsx b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/ssr/index.tsx new file mode 100644 index 00000000000..bb4fd212ce1 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/pages/pages/ssr/index.tsx @@ -0,0 +1,10 @@ +import { useRouter } from "next/router"; + +export const getServerSideProps = async () => { + return { props: { foo: 1 } }; +} + +export default function SSR() { + const { locale } = useRouter(); + return <>SSR {locale}; +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/styles/Home.module.css b/scripts/webframeworks-deploy-tests/nextjs/styles/Home.module.css new file mode 100644 index 00000000000..bd50f42ffe6 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/styles/Home.module.css @@ -0,0 +1,129 @@ +.container { + padding: 0 2rem; +} + +.main { + min-height: 100vh; + padding: 4rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.footer { + display: flex; + flex: 1; + padding: 2rem 0; + border-top: 1px solid #eaeaea; + justify-content: center; + align-items: center; +} + +.footer a { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; +} + +.title a { + color: #0070f3; + text-decoration: none; +} + +.title a:hover, +.title a:focus, +.title a:active { + text-decoration: underline; +} + +.title { + margin: 0; + line-height: 1.15; + font-size: 4rem; +} + +.title, +.description { + text-align: center; +} + +.description { + margin: 4rem 0; + line-height: 1.5; + font-size: 1.5rem; +} + +.code { + background: #fafafa; + border-radius: 5px; + padding: 0.75rem; + font-size: 1.1rem; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.grid { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + max-width: 800px; +} + +.card { + margin: 1rem; + padding: 1.5rem; + text-align: left; + color: inherit; + text-decoration: none; + border: 1px solid #eaeaea; + border-radius: 10px; + transition: color 0.15s ease, border-color 0.15s ease; + max-width: 300px; +} + +.card:hover, +.card:focus, +.card:active { + color: #0070f3; + border-color: #0070f3; +} + +.card h2 { + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.card p { + margin: 0; + font-size: 1.25rem; + line-height: 1.5; +} + +.logo { + height: 1em; + margin-left: 0.5rem; +} + +@media (max-width: 600px) { + .grid { + width: 100%; + flex-direction: column; + } +} + +@media (prefers-color-scheme: dark) { + .card, + .footer { + border-color: #222; + } + .code { + background: #111; + } + .logo img { + filter: invert(1); + } +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/styles/globals.css b/scripts/webframeworks-deploy-tests/nextjs/styles/globals.css new file mode 100644 index 00000000000..4f1842163d2 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/styles/globals.css @@ -0,0 +1,26 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } + body { + color: white; + background: black; + } +} diff --git a/scripts/webframeworks-deploy-tests/nextjs/tsconfig.json b/scripts/webframeworks-deploy-tests/nextjs/tsconfig.json new file mode 100644 index 00000000000..b25c4f834cb --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/scripts/webframeworks-deploy-tests/run.sh b/scripts/webframeworks-deploy-tests/run.sh new file mode 100755 index 00000000000..9876ad10bc9 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/run.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e # Immediately exit on failure + +# Globally link the CLI for the testing framework +./scripts/clean-install.sh + +source scripts/set-default-credentials.sh + +npm ci --prefix scripts/webframeworks-deploy-tests/nextjs +npm ci --prefix scripts/webframeworks-deploy-tests/angular +npm ci --prefix scripts/webframeworks-deploy-tests/functions + +FIREBASE_CLI_EXPERIMENTS=webframeworks,pintags firebase emulators:exec "mocha scripts/webframeworks-deploy-tests/tests.ts --exit --retries 2" --config scripts/webframeworks-deploy-tests/firebase.json --project demo-123 --debug diff --git a/scripts/webframeworks-deploy-tests/tests.ts b/scripts/webframeworks-deploy-tests/tests.ts new file mode 100644 index 00000000000..1fd2d5703c5 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/tests.ts @@ -0,0 +1,474 @@ +import { expect, use } from "chai"; +import { glob } from "glob"; +import { join, normalize, relative } from "path"; +import { readFileSync } from "fs"; +import fetch from "node-fetch"; +import type { NextConfig } from "next"; +import * as deepEqualUnordered from "deep-equal-in-any-order"; +use(deepEqualUnordered); + +import { getBuildId } from "../../src/frameworks/next/utils"; +import { fileExistsSync } from "../../src/fsutils"; +import { readFile } from "fs/promises"; + +const NEXT_OUTPUT_PATH = `${__dirname}/.firebase/demo-nextjs`; +const ANGULAR_OUTPUT_PATH = `${__dirname}/.firebase/demo-angular`; +const FIREBASE_EMULATOR_HUB = process.env.FIREBASE_EMULATOR_HUB; +const NEXT_BASE_PATH: NextConfig["basePath"] = "base"; +// TODO Angular basePath and i18n are not cooperating +const ANGULAR_BASE_PATH = ""; +const I18N_BASE = ""; +const DEFAULT_LANG = "en"; +const LOG_FILE = "firebase-debug.log"; +const NEXT_SOURCE = `${__dirname}/nextjs`; + +async function getFilesListFromDir(dir: string): Promise { + const files = await glob(`${dir}/**/*`, { nodir: true }); + return files.map((path) => relative(dir, path)); +} + +describe("webframeworks", function (this) { + this.timeout(10_000); + let NEXTJS_HOST: string; + let ANGULAR_HOST: string; + + before(async () => { + expect(FIREBASE_EMULATOR_HUB, "$FIREBASE_EMULATOR_HUB").to.not.be.empty; + const hubResponse = await fetch(`http://${FIREBASE_EMULATOR_HUB}/emulators`); + const { + hosting: { port, host }, + } = await hubResponse.json(); + NEXTJS_HOST = `http://${host}:${port}/${NEXT_BASE_PATH}`.replace(/\/$/, ""); + ANGULAR_HOST = `http://${host}:${port + 5}/${ANGULAR_BASE_PATH}`.replace(/\/$/, ""); + }); + + after(() => { + // This is not an empty block. + }); + + describe("build", () => { + it("should have the correct effective firebase.json", () => { + const result = readFileSync(LOG_FILE).toString(); + const effectiveFirebaseJSON = result + .split("[web frameworks] effective firebase.json: ") + .at(-1) + ?.split(new RegExp(`(\\[\\S+\\] )?\\[${new Date().getFullYear()}`))[0] + ?.trim(); + expect( + effectiveFirebaseJSON && JSON.parse(effectiveFirebaseJSON), + "firebase.json", + ).to.deep.equalInAnyOrder({ + hosting: [ + { + target: "nextjs", + source: "nextjs", + frameworksBackend: { + maxInstances: 1, + region: "asia-east1", + }, + rewrites: [ + { + destination: "/base", + source: "/base/about", + }, + { + source: "/base/**", + function: { + functionId: "ssrdemonextjs", + region: "asia-east1", + pinTag: true, + }, + }, + { + function: { + functionId: "helloWorld", + }, + source: "helloWorld", + }, + ], + site: "demo-nextjs", + redirects: [ + { + destination: "/base/", + source: "/base/en/about", + type: 308, + }, + { + destination: "/base", + source: "/base/about", + type: 308, + }, + ], + headers: [ + { + headers: [ + { + key: "x-custom-header", + value: "my custom header value", + }, + { + key: "x-another-custom-header", + value: "my other custom header value", + }, + ], + source: "/base/about", + }, + { + source: "/base/app/api/static", + headers: [ + { + key: "content-type", + value: "application/json", + }, + { + key: "custom-header", + value: "custom-value", + }, + { + key: "x-next-cache-tags", + value: + "_N_T_/layout,_N_T_/app/layout,_N_T_/app/api/layout,_N_T_/app/api/static/layout,_N_T_/app/api/static/route,_N_T_/app/api/static", + }, + ], + }, + { + headers: [ + { + key: "x-next-cache-tags", + value: + "_N_T_/layout,_N_T_/app/layout,_N_T_/app/image/layout,_N_T_/app/image/page,_N_T_/app/image", + }, + ], + source: "/base/app/image", + }, + { + headers: [ + { + key: "x-next-cache-tags", + value: + "_N_T_/layout,_N_T_/app/layout,_N_T_/app/ssg/layout,_N_T_/app/ssg/page,_N_T_/app/ssg", + }, + ], + source: "/base/app/ssg", + }, + { + headers: [ + { + key: "x-next-cache-tags", + value: + "_N_T_/layout,_N_T_/app/layout,_N_T_/app/isr/layout,_N_T_/app/isr/page,_N_T_/app/isr", + }, + ], + source: "/base/app/isr", + }, + ], + cleanUrls: true, + trailingSlash: false, + i18n: { + root: "/", + }, + public: ".firebase/demo-nextjs/hosting", + webFramework: "next_ssr", + }, + { + target: "angular", + source: "angular", + frameworksBackend: { + maxInstances: 1, + region: "europe-west1", + }, + rewrites: [ + { + function: { + functionId: "helloWorld", + }, + source: "helloWorld", + }, + { + source: "/**", + function: { + functionId: "ssrdemoangular", + region: "europe-west1", + pinTag: true, + }, + }, + ], + site: "demo-angular", + redirects: [], + headers: [], + cleanUrls: true, + i18n: { + root: "/", + }, + public: ".firebase/demo-angular/hosting", + webFramework: "angular_ssr", + }, + ], + functions: [ + { + codebase: "default", + ignore: ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log"], + source: "functions", + }, + { + codebase: "firebase-frameworks-demo-nextjs", + source: ".firebase/demo-nextjs/functions", + }, + { + codebase: "firebase-frameworks-demo-angular", + source: ".firebase/demo-angular/functions", + }, + ], + }); + }); + }); + + describe("next.js", () => { + describe("app directory", () => { + it("should have working static routes", async () => { + const apiStaticJSON = JSON.parse( + readFileSync(`${NEXT_OUTPUT_PATH}/hosting/${NEXT_BASE_PATH}/app/api/static`).toString(), + ); + const apiStaticResponse = await fetch(`${NEXTJS_HOST}/app/api/static`); + expect(apiStaticResponse.ok).to.be.true; + expect(apiStaticResponse.headers.get("content-type")).to.eql("application/json"); + expect(apiStaticResponse.headers.get("custom-header")).to.eql("custom-value"); + expect(await apiStaticResponse.json()).to.eql(apiStaticJSON); + }); + + it("should have working SSG", async () => { + const fooResponse = await fetch(`${NEXTJS_HOST}/app/ssg`); + expect(fooResponse.ok).to.be.true; + const fooResponseText = await fooResponse.text(); + + const fooHtml = readFileSync( + `${NEXT_OUTPUT_PATH}/hosting/${NEXT_BASE_PATH}/app/ssg.html`, + ).toString(); + expect(fooHtml).to.eql(fooResponseText); + }); + + it("should have working ISR", async () => { + const response = await fetch(`${NEXTJS_HOST}/app/isr`); + expect(response.ok).to.be.true; + expect(response.headers.get("cache-control")).to.eql( + "private, no-cache, no-store, max-age=0, must-revalidate", + ); + expect(await response.text()).to.include("ISR"); + }); + + it("should have working SSR", async () => { + const bazResponse = await fetch(`${NEXTJS_HOST}/app/ssr`); + expect(bazResponse.ok).to.be.true; + expect(await bazResponse.text()).to.include("SSR"); + }); + + it("should have working dynamic routes", async () => { + const apiDynamicResponse = await fetch(`${NEXTJS_HOST}/app/api/dynamic`); + expect(apiDynamicResponse.ok).to.be.true; + expect(apiDynamicResponse.headers.get("cache-control")).to.eql("private"); + expect(await apiDynamicResponse.json()).to.eql([1, 2, 3]); + }); + + it("should have working image", async () => { + const response = await fetch(`${NEXTJS_HOST}/app/image`); + expect(response.ok).to.be.true; + expect(await response.text()).to.include(" { + for (const lang of [undefined, "en", "fr"]) { + const headers = lang ? { "Accept-Language": lang } : undefined; + + describe(`${lang || "default"} locale`, () => { + it("should have working i18n", async () => { + const response = await fetch(`${NEXTJS_HOST}`, { headers }); + expect(response.ok).to.be.true; + expect(await response.text()).to.include(``); + }); + + it("should have working SSG", async () => { + const response = await fetch(`${NEXTJS_HOST}/pages/ssg`, { headers }); + expect(response.ok).to.be.true; + expect(await response.text()).to.include(`SSG ${lang || DEFAULT_LANG}`); + }); + }); + } + + it("should have working SSR", async () => { + const response = await fetch(`${NEXTJS_HOST}/api/hello`); + expect(response.ok).to.be.true; + expect(await response.json()).to.eql({ name: "John Doe" }); + }); + + it("should have working ISR", async () => { + const response = await fetch(`${NEXTJS_HOST}/pages/isr`); + expect(response.ok).to.be.true; + expect(response.headers.get("cache-control")).to.eql("private"); + expect(await response.text()).to.include(`ISR ${DEFAULT_LANG}`); + }); + }); + + it("should log reasons for backend", () => { + const result = readFileSync(LOG_FILE).toString(); + + expect(result, "build result").to.include( + "Building a Cloud Function to run this application. This is needed due to:", + ); + expect(result, "build result").to.include(" • middleware"); + expect(result, "build result").to.include(" • Image Optimization"); + expect(result, "build result").to.include(" • use of fallback /pages/fallback/[id]"); + expect(result, "build result").to.include(" • use of revalidate /app/isr"); + expect(result, "build result").to.include(" • non-static route /api/hello"); + expect(result, "build result").to.include(" • non-static route /pages/ssr"); + expect(result, "build result").to.include(" • non-static component /app/api/dynamic/route"); + expect(result, "build result").to.include(" • non-static component /app/ssr/page"); + }); + + it("should have the expected static files to be deployed", async () => { + const buildId = await getBuildId(join(NEXT_SOURCE, ".next")); + + const EXPECTED_FILES = ["", "en", "fr"] + .flatMap((locale) => [ + ...(locale + ? [ + `/${NEXT_BASE_PATH}/_next/data/${buildId}/${locale}/pages/fallback/1.json`, + `/${NEXT_BASE_PATH}/_next/data/${buildId}/${locale}/pages/fallback/2.json`, + ] + : [ + `/${NEXT_BASE_PATH}/_next/data/${buildId}/pages/ssg.json`, + `/${NEXT_BASE_PATH}/_next/static/${buildId}/_buildManifest.js`, + `/${NEXT_BASE_PATH}/_next/static/${buildId}/_ssgManifest.js`, + `/${NEXT_BASE_PATH}/app/api/static`, + `/${NEXT_BASE_PATH}/app/image.html`, + `/${NEXT_BASE_PATH}/app/ssg.html`, + `/${NEXT_BASE_PATH}/404.html`, + ]), + `/${I18N_BASE}/${locale}/${NEXT_BASE_PATH}/pages/fallback/1.html`, + `/${I18N_BASE}/${locale}/${NEXT_BASE_PATH}/pages/fallback/2.html`, + `/${I18N_BASE}/${locale}/${NEXT_BASE_PATH}/pages/ssg.html`, + // TODO(jamesdaniels) figure out why 404 isn't being translated + // `/${I18N_BASE}/${locale}/${NEXT_BASE_PATH}/404.html`, + `/${I18N_BASE}/${locale}/${NEXT_BASE_PATH}/500.html`, + `/${I18N_BASE}/${locale}/${NEXT_BASE_PATH}/index.html`, + ]) + .map(normalize) + .map((it) => (it.startsWith("/") ? it.substring(1) : it)); + + const EXPECTED_PATTERNS = [ + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/[^-]+-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/app\/layout-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/main-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/main-app-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/pages\/_app-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/pages\/_error-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/pages\/index-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/polyfills-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/chunks\/webpack-[^\.]+\.js`, + `${NEXT_BASE_PATH}\/_next\/static\/css\/[^\.]+\.css`, + ].map((it) => new RegExp(it)); + + const files = await getFilesListFromDir(`${NEXT_OUTPUT_PATH}/hosting`); + const unmatchedFiles = files.filter( + (it) => + !( + EXPECTED_FILES.includes(it) || EXPECTED_PATTERNS.some((pattern) => !!it.match(pattern)) + ), + ); + const unmatchedExpectations = [ + ...EXPECTED_FILES.filter((it) => !files.includes(it)), + ...EXPECTED_PATTERNS.filter((it) => !files.some((file) => !!file.match(it))), + ]; + + expect(unmatchedFiles, "matchedFiles").to.eql([]); + expect(unmatchedExpectations, "unmatchedExpectations").to.eql([]); + }); + + it("should not have development files to be deployed", async () => { + const distDir = ".next"; + + const UNEXPECTED_PATTERNS = [ + `${distDir}\/cache\/.*-development`, + `${distDir}\/cache\/eslint`, + ].map((it) => new RegExp(it)); + + const files = await getFilesListFromDir(`${NEXT_OUTPUT_PATH}/functions/${distDir}`); + + const filesContainingUnexpectedPatterns = UNEXPECTED_PATTERNS.filter((unexpectedPattern) => + files.some((file) => file.match(unexpectedPattern)), + ); + + expect(filesContainingUnexpectedPatterns.length).to.eql(0); + }); + }); + + describe("angular", () => { + for (const lang of [undefined, "en", "fr", "es"]) { + const headers = lang ? { "Accept-Language": lang } : undefined; + + describe(`${lang || "default"} locale`, () => { + it("should have working SSG", async () => { + const path = `${ANGULAR_OUTPUT_PATH}/hosting/${I18N_BASE}/${ + lang || "" + }/${ANGULAR_BASE_PATH}/index.html`; + expect(fileExistsSync(path)).to.be.true; + const contents = (await readFile(path)).toString(); + expect(contents).to.include(` { + const response = await fetch(`${ANGULAR_HOST}/foo/1`, { headers }); + expect(response.ok).to.be.true; + const body = await response.text(); + expect(body).to.include(` { + const EXPECTED_FILES = ["", "en", "fr", "es"] + .flatMap((locale) => [ + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/index.html`, + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/3rdpartylicenses.txt`, + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/favicon.ico`, + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/index.original.html`, + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/3rdpartylicenses.txt`, + ]) + .map(normalize) + .map((it) => (it.startsWith("/") ? it.substring(1) : it)); + + const EXPECTED_PATTERNS = ["", "en", "fr", "es"] + .flatMap((locale) => [ + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/main\.[^\.]+\.js`, + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/polyfills\.[^\.]+\.js`, + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/runtime\.[^\.]+\.js`, + `/${I18N_BASE}/${locale}/${ANGULAR_BASE_PATH}/styles\.[^\.]+\.css`, + ]) + .map(normalize) + .map((it) => (it.startsWith("/") ? it.substring(1) : it)) + .map((it) => new RegExp(it.replace("/", "\\/"))); + + const files = await getFilesListFromDir(`${ANGULAR_OUTPUT_PATH}/hosting`); + const unmatchedFiles = files.filter( + (it) => + !( + EXPECTED_FILES.includes(it) || EXPECTED_PATTERNS.some((pattern) => !!it.match(pattern)) + ), + ); + const unmatchedExpectations = [ + ...EXPECTED_FILES.filter((it) => !files.includes(it)), + ...EXPECTED_PATTERNS.filter((it) => !files.some((file) => !!file.match(it))), + ]; + + expect(unmatchedFiles, "matchedFiles").to.eql([]); + expect(unmatchedExpectations, "unmatchedExpectations").to.eql([]); + }); + }); +}); diff --git a/src/accountExporter.js b/src/accountExporter.js deleted file mode 100644 index 1406df9feb1..00000000000 --- a/src/accountExporter.js +++ /dev/null @@ -1,203 +0,0 @@ -"use strict"; - -var os = require("os"); -var path = require("path"); -var _ = require("lodash"); - -var api = require("./api"); -var utils = require("./utils"); -var { FirebaseError } = require("./error"); - -// TODO: support for MFA at runtime was added in PR #3173, but this exporter currently ignores `mfaInfo` and loses the data on export. -var EXPORTED_JSON_KEYS = [ - "localId", - "email", - "emailVerified", - "passwordHash", - "salt", - "displayName", - "photoUrl", - "lastLoginAt", - "createdAt", - "phoneNumber", - "disabled", - "customAttributes", -]; -var EXPORTED_JSON_KEYS_RENAMING = { - lastLoginAt: "lastSignedInAt", -}; -var EXPORTED_PROVIDER_USER_INFO_KEYS = ["providerId", "rawId", "email", "displayName", "photoUrl"]; -var PROVIDER_ID_INDEX_MAP = { - "google.com": 7, - "facebook.com": 11, - "twitter.com": 15, - "github.com": 19, -}; - -var _escapeComma = function (str) { - if (str.indexOf(",") !== -1) { - // Encapsulate the string with quotes if it contains a comma. - return `"${str}"`; - } - return str; -}; - -var _convertToNormalBase64 = function (data) { - return data.replace(/_/g, "/").replace(/-/g, "+"); -}; - -var _addProviderUserInfo = function (providerInfo, arr, startPos) { - arr[startPos] = providerInfo.rawId; - arr[startPos + 1] = providerInfo.email || ""; - arr[startPos + 2] = _escapeComma(providerInfo.displayName || ""); - arr[startPos + 3] = providerInfo.photoUrl || ""; -}; - -var _transUserToArray = function (user) { - var arr = Array(27).fill(""); - arr[0] = user.localId; - arr[1] = user.email || ""; - arr[2] = user.emailVerified || false; - arr[3] = _convertToNormalBase64(user.passwordHash || ""); - arr[4] = _convertToNormalBase64(user.salt || ""); - arr[5] = _escapeComma(user.displayName || ""); - arr[6] = user.photoUrl || ""; - for (var i = 0; i < (!user.providerUserInfo ? 0 : user.providerUserInfo.length); i++) { - var providerInfo = user.providerUserInfo[i]; - if (providerInfo && PROVIDER_ID_INDEX_MAP[providerInfo.providerId]) { - _addProviderUserInfo(providerInfo, arr, PROVIDER_ID_INDEX_MAP[providerInfo.providerId]); - } - } - arr[23] = user.createdAt; - arr[24] = user.lastLoginAt; - arr[25] = user.phoneNumber; - arr[26] = user.disabled; - arr[27] = user.customAttributes; - return arr; -}; - -var _transUserJson = function (user) { - var newUser = {}; - _.each(_.pick(user, EXPORTED_JSON_KEYS), function (value, key) { - var newKey = EXPORTED_JSON_KEYS_RENAMING[key] || key; - newUser[newKey] = value; - }); - if (newUser.passwordHash) { - newUser.passwordHash = _convertToNormalBase64(newUser.passwordHash); - } - if (newUser.salt) { - newUser.salt = _convertToNormalBase64(newUser.salt); - } - if (user.providerUserInfo) { - newUser.providerUserInfo = []; - user.providerUserInfo.forEach(function (providerInfo) { - if (!_.includes(Object.keys(PROVIDER_ID_INDEX_MAP), providerInfo.providerId)) { - return; - } - newUser.providerUserInfo.push(_.pick(providerInfo, EXPORTED_PROVIDER_USER_INFO_KEYS)); - }); - } - return newUser; -}; - -var validateOptions = function (options, fileName) { - var exportOptions = {}; - if (fileName === undefined) { - throw new FirebaseError("Must specify data file", { exit: 1 }); - } - var extName = path.extname(fileName.toLowerCase()); - if (extName === ".csv") { - exportOptions.format = "csv"; - } else if (extName === ".json") { - exportOptions.format = "json"; - } else if (options.format) { - var format = options.format.toLowerCase(); - if (format === "csv" || format === "json") { - exportOptions.format = format; - } else { - throw new FirebaseError("Unsupported data file format, should be csv or json", { exit: 1 }); - } - } else { - throw new FirebaseError( - "Please specify data file format in file name, or use `format` parameter", - { - exit: 1, - } - ); - } - return exportOptions; -}; - -var _createWriteUsersToFile = function () { - var jsonSep = ""; - return function (userList, format, writeStream) { - userList.map(function (user) { - if (user.passwordHash && user.version !== 0) { - // Password isn't hashed by default Scrypt. - delete user.passwordHash; - delete user.salt; - } - if (format === "csv") { - writeStream.write(_transUserToArray(user).join(",") + "," + os.EOL, "utf8"); - } else { - writeStream.write(jsonSep + JSON.stringify(_transUserJson(user), null, 2), "utf8"); - jsonSep = "," + os.EOL; - } - }); - }; -}; - -var serialExportUsers = function (projectId, options) { - if (!options.writeUsersToFile) { - options.writeUsersToFile = _createWriteUsersToFile(); - } - var postBody = { - targetProjectId: projectId, - maxResults: options.batchSize, - }; - if (options.nextPageToken) { - postBody.nextPageToken = options.nextPageToken; - } - if (!options.timeoutRetryCount) { - options.timeoutRetryCount = 0; - } - return api - .request("POST", "/identitytoolkit/v3/relyingparty/downloadAccount", { - auth: true, - json: true, - data: postBody, - origin: api.googleOrigin, - }) - .then(function (ret) { - options.timeoutRetryCount = 0; - var userList = ret.body.users; - if (userList && userList.length > 0) { - options.writeUsersToFile(userList, options.format, options.writeStream); - utils.logSuccess("Exported " + userList.length + " account(s) successfully."); - // The identitytoolkit API do not return a nextPageToken value - // consistently when the last page is reached - if (!ret.body.nextPageToken) { - return; - } - options.nextPageToken = ret.body.nextPageToken; - return serialExportUsers(projectId, options); - } - }) - .catch((err) => { - // Calling again in case of error timedout so that script won't exit - if (err.original.code === "ETIMEDOUT") { - options.timeoutRetryCount++; - if (options.timeoutRetryCount > 5) { - return err; - } - return serialExportUsers(projectId, options); - } - }); -}; - -var accountExporter = { - validateOptions: validateOptions, - serialExportUsers: serialExportUsers, -}; - -module.exports = accountExporter; diff --git a/src/accountExporter.spec.ts b/src/accountExporter.spec.ts new file mode 100644 index 00000000000..6c8fd316b3a --- /dev/null +++ b/src/accountExporter.spec.ts @@ -0,0 +1,278 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { expect } from "chai"; +import * as nock from "nock"; +import * as os from "os"; +import * as sinon from "sinon"; + +import { validateOptions, serialExportUsers } from "./accountExporter"; + +describe("accountExporter", () => { + describe("validateOptions", () => { + it("should reject when no format provided", () => { + expect(() => validateOptions({}, "output_file")).to.throw(); + }); + + it("should reject when format is not csv or json", () => { + expect(() => validateOptions({ format: "txt" }, "output_file")).to.throw(); + }); + + it("should ignore format param when implicitly specified in file name", () => { + const ret = validateOptions({ format: "JSON" }, "output_file.csv"); + expect(ret.format).to.eq("csv"); + }); + + it("should use format param when not implicitly specified in file name", () => { + const ret = validateOptions({ format: "JSON" }, "output_file"); + expect(ret.format).to.eq("json"); + }); + }); + + describe("serialExportUsers", () => { + let sandbox: sinon.SinonSandbox; + let userList: { + localId: string; + email: string; + displayName: string; + disabled: boolean; + customAttributes?: string; + }[] = []; + const writeStream = { + write: () => {}, + end: () => {}, + }; + let spyWrite: sinon.SinonSpy; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + spyWrite = sandbox.spy(writeStream, "write"); + for (let i = 0; i < 7; i++) { + userList.push({ + localId: i.toString(), + email: "test" + i + "@test.org", + displayName: "John Tester" + i, + disabled: i % 2 === 0, + }); + } + }); + + afterEach(() => { + sandbox.restore(); + nock.cleanAll(); + userList = []; + }); + + it("should call api.request multiple times for JSON export", async () => { + mockAllUsersRequests(); + + await serialExportUsers("test-project-id", { + format: "JSON", + batchSize: 3, + writeStream: writeStream, + }); + expect(spyWrite.callCount).to.eq(7); + expect(spyWrite.getCall(0).args[0]).to.eq(JSON.stringify(userList[0], null, 2)); + for (let j = 1; j < 7; j++) { + expect(spyWrite.getCall(j).args[0]).to.eq( + "," + os.EOL + JSON.stringify(userList[j], null, 2), + ); + } + expect(nock.isDone()).to.be.true; + }); + + it("should call api.request multiple times for CSV export", async () => { + mockAllUsersRequests(); + + await serialExportUsers("test-project-id", { + format: "csv", + batchSize: 3, + writeStream: writeStream, + }); + expect(spyWrite.callCount).to.eq(userList.length); + for (let j = 0; j < userList.length; j++) { + const expectedEntry = + userList[j].localId + + "," + + userList[j].email + + ",false,,," + + userList[j].displayName + + Array(22).join(",") + // A lot of empty fields... + userList[j].disabled; + expect(spyWrite.getCall(j).args[0]).to.eq(expectedEntry + ",," + os.EOL); + } + expect(nock.isDone()).to.be.true; + }); + + it("should encapsulate displayNames with commas for csv formats", async () => { + // Initialize user with comma in display name. + const singleUser = { + localId: "1", + email: "test1@test.org", + displayName: "John Tester1, CFA", + disabled: false, + }; + nock("https://www.googleapis.com") + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 1, + targetProjectId: "test-project-id", + }) + .reply(200, { + users: [singleUser], + nextPageToken: "1", + }) + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 1, + nextPageToken: "1", + targetProjectId: "test-project-id", + }) + .reply(200, { + users: [], + nextPageToken: "1", + }); + + await serialExportUsers("test-project-id", { + format: "csv", + batchSize: 1, + writeStream: writeStream, + }); + expect(spyWrite.callCount).to.eq(1); + const expectedEntry = + singleUser.localId + + "," + + singleUser.email + + ",false,,," + + '"' + + singleUser.displayName + + '"' + + Array(22).join(",") + // A lot of empty fields. + singleUser.disabled; + expect(spyWrite.getCall(0).args[0]).to.eq(expectedEntry + ",," + os.EOL); + expect(nock.isDone()).to.be.true; + }); + + it("should not emit redundant comma in JSON on consecutive calls", async () => { + mockAllUsersRequests(); + + const correctString = + '{\n "localId": "0",\n "email": "test0@test.org",\n "displayName": "John Tester0",\n "disabled": true\n}'; + + const firstWriteSpy = sinon.spy(); + await serialExportUsers("test-project-id", { + format: "JSON", + batchSize: 3, + writeStream: { write: firstWriteSpy, end: () => {} }, + }); + expect(firstWriteSpy.args[0][0]).to.be.eq( + correctString, + "The first call did not emit the correct string", + ); + + mockAllUsersRequests(); + + const secondWriteSpy = sinon.spy(); + await serialExportUsers("test-project-id", { + format: "JSON", + batchSize: 3, + writeStream: { write: secondWriteSpy, end: () => {} }, + }); + expect(secondWriteSpy.args[0][0]).to.be.eq( + correctString, + "The second call did not emit the correct string", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should export a user's custom attributes for JSON formats", async () => { + userList[0].customAttributes = + '{ "customBoolean": true, "customString": "test", "customInt": 99 }'; + userList[1].customAttributes = + '{ "customBoolean": true, "customString2": "test2", "customInt": 99 }'; + nock("https://www.googleapis.com") + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 3, + targetProjectId: "test-project-id", + }) + .reply(200, { + users: userList.slice(0, 3), + }); + await serialExportUsers("test-project-id", { + format: "JSON", + batchSize: 3, + writeStream: writeStream, + }); + expect(spyWrite.getCall(0).args[0]).to.eq(JSON.stringify(userList[0], null, 2)); + expect(spyWrite.getCall(1).args[0]).to.eq( + "," + os.EOL + JSON.stringify(userList[1], null, 2), + ); + expect(spyWrite.getCall(2).args[0]).to.eq( + "," + os.EOL + JSON.stringify(userList[2], null, 2), + ); + expect(nock.isDone()).to.be.true; + }); + + it("should export a user's custom attributes for CSV formats", async () => { + userList[0].customAttributes = + '{ "customBoolean": true, "customString": "test", "customInt": 99 }'; + userList[1].customAttributes = '{ "customBoolean": true }'; + nock("https://www.googleapis.com") + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 3, + targetProjectId: "test-project-id", + }) + .reply(200, { + users: userList.slice(0, 3), + }); + await serialExportUsers("test-project-id", { + format: "JSON", + batchSize: 3, + writeStream: writeStream, + }); + expect(spyWrite.getCall(0).args[0]).to.eq(JSON.stringify(userList[0], null, 2)); + expect(spyWrite.getCall(1).args[0]).to.eq( + "," + os.EOL + JSON.stringify(userList[1], null, 2), + ); + expect(spyWrite.getCall(2).args[0]).to.eq( + "," + os.EOL + JSON.stringify(userList[2], null, 2), + ); + expect(nock.isDone()).to.be.true; + }); + + function mockAllUsersRequests(): void { + nock("https://www.googleapis.com") + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 3, + targetProjectId: "test-project-id", + }) + .reply(200, { + users: userList.slice(0, 3), + nextPageToken: "3", + }) + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 3, + nextPageToken: "3", + targetProjectId: "test-project-id", + }) + .reply(200, { + users: userList.slice(3, 6), + nextPageToken: "6", + }) + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 3, + nextPageToken: "6", + targetProjectId: "test-project-id", + }) + .reply(200, { + users: userList.slice(6, 7), + nextPageToken: "7", + }) + .post("/identitytoolkit/v3/relyingparty/downloadAccount", { + maxResults: 3, + nextPageToken: "7", + targetProjectId: "test-project-id", + }) + .reply(200, { + users: [], + nextPageToken: "7", + }); + } + }); +}); diff --git a/src/accountExporter.ts b/src/accountExporter.ts new file mode 100644 index 00000000000..2f19966d620 --- /dev/null +++ b/src/accountExporter.ts @@ -0,0 +1,223 @@ +import { Writable } from "stream"; +import * as os from "os"; +import * as path from "path"; + +import { Client } from "./apiv2"; +import { FirebaseError } from "./error"; +import { googleOrigin } from "./api"; +import * as utils from "./utils"; + +const apiClient = new Client({ + urlPrefix: googleOrigin(), +}); + +// TODO: support for MFA at runtime was added in PR #3173, but this exporter currently ignores `mfaInfo` and loses the data on export. +const EXPORTED_JSON_KEYS = [ + "localId", + "email", + "emailVerified", + "passwordHash", + "salt", + "displayName", + "photoUrl", + "lastLoginAt", + "createdAt", + "phoneNumber", + "disabled", + "customAttributes", +]; +const EXPORTED_JSON_KEYS_RENAMING: Record = { + lastLoginAt: "lastSignedInAt", +}; +const EXPORTED_PROVIDER_USER_INFO_KEYS = [ + "providerId", + "rawId", + "email", + "displayName", + "photoUrl", +]; +const PROVIDER_ID_INDEX_MAP = new Map([ + ["google.com", 7], + ["facebook.com", 11], + ["twitter.com", 15], + ["github.com", 19], +]); + +function escapeComma(str: string): string { + if (str.includes(",")) { + // Encapsulate the string with quotes if it contains a comma. + return `"${str}"`; + } + return str; +} + +function convertToNormalBase64(data: string): string { + return data.replace(/_/g, "/").replace(/-/g, "+"); +} + +function addProviderUserInfo(providerInfo: any, arr: any[], startPos: number): void { + arr[startPos] = providerInfo.rawId; + arr[startPos + 1] = providerInfo.email || ""; + arr[startPos + 2] = escapeComma(providerInfo.displayName || ""); + arr[startPos + 3] = providerInfo.photoUrl || ""; +} + +function transUserToArray(user: any): any[] { + const arr = Array(27).fill(""); + arr[0] = user.localId; + arr[1] = user.email || ""; + arr[2] = user.emailVerified || false; + arr[3] = convertToNormalBase64(user.passwordHash || ""); + arr[4] = convertToNormalBase64(user.salt || ""); + arr[5] = escapeComma(user.displayName || ""); + arr[6] = user.photoUrl || ""; + for (let i = 0; i < (!user.providerUserInfo ? 0 : user.providerUserInfo.length); i++) { + const providerInfo = user.providerUserInfo[i]; + if (providerInfo) { + const providerIndex = PROVIDER_ID_INDEX_MAP.get(providerInfo.providerId); + if (providerIndex) { + addProviderUserInfo(providerInfo, arr, providerIndex); + } + } + } + arr[23] = user.createdAt; + arr[24] = user.lastLoginAt; + arr[25] = user.phoneNumber; + arr[26] = user.disabled; + // quote entire custom claims object and escape inner quotes with quotes + arr[27] = user.customAttributes + ? `"${user.customAttributes.replace(/(? = {}; + for (const k of EXPORTED_JSON_KEYS) { + pickedUser[k] = user[k]; + } + for (const [key, value] of Object.entries(pickedUser)) { + const newKey = EXPORTED_JSON_KEYS_RENAMING[key] || key; + newUser[newKey] = value; + } + if (newUser.passwordHash) { + newUser.passwordHash = convertToNormalBase64(newUser.passwordHash); + } + if (newUser.salt) { + newUser.salt = convertToNormalBase64(newUser.salt); + } + if (user.providerUserInfo) { + newUser.providerUserInfo = []; + for (const providerInfo of user.providerUserInfo) { + if (PROVIDER_ID_INDEX_MAP.has(providerInfo.providerId)) { + const picked: Record = {}; + for (const k of EXPORTED_PROVIDER_USER_INFO_KEYS) { + picked[k] = providerInfo[k]; + } + newUser.providerUserInfo.push(picked); + } + } + } + return newUser; +} + +export function validateOptions(options: any, fileName: string): any { + const exportOptions: any = {}; + if (fileName === undefined) { + throw new FirebaseError("Must specify data file"); + } + const extName = path.extname(fileName.toLowerCase()); + if (extName === ".csv") { + exportOptions.format = "csv"; + } else if (extName === ".json") { + exportOptions.format = "json"; + } else if (options.format) { + const format = options.format.toLowerCase(); + if (format === "csv" || format === "json") { + exportOptions.format = format; + } else { + throw new FirebaseError("Unsupported data file format, should be csv or json"); + } + } else { + throw new FirebaseError( + "Please specify data file format in file name, or use `format` parameter", + ); + } + return exportOptions; +} + +function createWriteUsersToFile(): ( + userList: any[], + format: "csv" | "json", + writeStream: Writable, +) => void { + let jsonSep = ""; + return (userList: any[], format: "csv" | "json", writeStream: Writable) => { + userList.map((user) => { + if (user.passwordHash && user.version !== 0) { + // Password isn't hashed by default Scrypt. + delete user.passwordHash; + delete user.salt; + } + if (format === "csv") { + writeStream.write(transUserToArray(user).join(",") + "," + os.EOL, "utf8"); + } else { + writeStream.write(jsonSep + JSON.stringify(transUserJson(user), null, 2), "utf8"); + jsonSep = "," + os.EOL; + } + }); + }; +} + +export async function serialExportUsers(projectId: string, options: any): Promise { + if (!options.writeUsersToFile) { + options.writeUsersToFile = createWriteUsersToFile(); + } + const postBody: any = { + targetProjectId: projectId, + maxResults: options.batchSize, + }; + if (options.nextPageToken) { + postBody.nextPageToken = options.nextPageToken; + } + if (!options.timeoutRetryCount) { + options.timeoutRetryCount = 0; + } + try { + const ret = await apiClient.post( + "/identitytoolkit/v3/relyingparty/downloadAccount", + postBody, + { + skipLog: { resBody: true }, // This contains a lot of PII - don't log it. + }, + ); + options.timeoutRetryCount = 0; + const userList = ret.body.users; + if (userList && userList.length > 0) { + options.writeUsersToFile(userList, options.format, options.writeStream); + utils.logSuccess("Exported " + userList.length + " account(s) successfully."); + // The identitytoolkit API do not return a nextPageToken value + // consistently when the last page is reached + if (!ret.body.nextPageToken) { + return; + } + options.nextPageToken = ret.body.nextPageToken; + return serialExportUsers(projectId, options); + } + } catch (err: any) { + // Calling again in case of error timedout so that script won't exit + if (err.original?.code === "ETIMEDOUT") { + options.timeoutRetryCount++; + if (options.timeoutRetryCount > 5) { + return err; + } + return serialExportUsers(projectId, options); + } + if (err instanceof FirebaseError) { + throw err; + } else { + throw new FirebaseError(`Failed to export accounts: ${err}`, { original: err }); + } + } +} diff --git a/src/accountImporter.js b/src/accountImporter.js deleted file mode 100644 index f25fb3cd932..00000000000 --- a/src/accountImporter.js +++ /dev/null @@ -1,345 +0,0 @@ -"use strict"; - -var clc = require("cli-color"); -var _ = require("lodash"); - -var api = require("./api"); -const { logger } = require("./logger"); -var utils = require("./utils"); -var { FirebaseError } = require("./error"); - -// TODO: support for MFA at runtime was added in PR #3173, but this importer currently ignores `mfaInfo` and loses the data on import. -var ALLOWED_JSON_KEYS = [ - "localId", - "email", - "emailVerified", - "passwordHash", - "salt", - "displayName", - "photoUrl", - "createdAt", - "lastSignedInAt", - "providerUserInfo", - "phoneNumber", - "disabled", - "customAttributes", -]; -var ALLOWED_JSON_KEYS_RENAMING = { - lastSignedInAt: "lastLoginAt", -}; -var ALLOWED_PROVIDER_USER_INFO_KEYS = ["providerId", "rawId", "email", "displayName", "photoUrl"]; -var ALLOWED_PROVIDER_IDS = ["google.com", "facebook.com", "twitter.com", "github.com"]; - -var _isValidBase64 = function (str) { - var expected = Buffer.from(str, "base64").toString("base64"); - // Buffer automatically pads with '=' character, - // but input string might not have padding. - if (str.length < expected.length && str.slice(-1) !== "=") { - str += "=".repeat(expected.length - str.length); - } - return expected === str; -}; - -var _toWebSafeBase64 = function (data) { - return data.toString("base64").replace(/\//g, "_").replace(/\+/g, "-"); -}; - -var _addProviderUserInfo = function (user, providerId, arr) { - if (arr[0]) { - user.providerUserInfo.push({ - providerId: providerId, - rawId: arr[0], - email: arr[1], - displayName: arr[2], - photoUrl: arr[3], - }); - } -}; - -var _genUploadAccountPostBody = function (projectId, accounts, hashOptions) { - var postBody = { - users: accounts.map(function (account) { - if (account.passwordHash) { - account.passwordHash = _toWebSafeBase64(account.passwordHash); - } - if (account.salt) { - account.salt = _toWebSafeBase64(account.salt); - } - _.each(ALLOWED_JSON_KEYS_RENAMING, function (value, key) { - if (account[key]) { - account[value] = account[key]; - delete account[key]; - } - }); - return account; - }), - }; - if (hashOptions.hashAlgo) { - postBody.hashAlgorithm = hashOptions.hashAlgo; - } - if (hashOptions.hashKey) { - postBody.signerKey = _toWebSafeBase64(hashOptions.hashKey); - } - if (hashOptions.saltSeparator) { - postBody.saltSeparator = _toWebSafeBase64(hashOptions.saltSeparator); - } - if (hashOptions.rounds) { - postBody.rounds = hashOptions.rounds; - } - if (hashOptions.memCost) { - postBody.memoryCost = hashOptions.memCost; - } - if (hashOptions.cpuMemCost) { - postBody.cpuMemCost = hashOptions.cpuMemCost; - } - if (hashOptions.parallelization) { - postBody.parallelization = hashOptions.parallelization; - } - if (hashOptions.blockSize) { - postBody.blockSize = hashOptions.blockSize; - } - if (hashOptions.dkLen) { - postBody.dkLen = hashOptions.dkLen; - } - if (hashOptions.passwordHashOrder) { - postBody.passwordHashOrder = hashOptions.passwordHashOrder; - } - postBody.targetProjectId = projectId; - return postBody; -}; - -var transArrayToUser = function (arr) { - var user = { - localId: arr[0], - email: arr[1], - emailVerified: arr[2] === "true", - passwordHash: arr[3], - salt: arr[4], - displayName: arr[5], - photoUrl: arr[6], - createdAt: arr[23], - lastLoginAt: arr[24], - phoneNumber: arr[25], - providerUserInfo: [], - disabled: arr[26], - customAttributes: arr[27], - }; - _addProviderUserInfo(user, "google.com", arr.slice(7, 11)); - _addProviderUserInfo(user, "facebook.com", arr.slice(11, 15)); - _addProviderUserInfo(user, "twitter.com", arr.slice(15, 19)); - _addProviderUserInfo(user, "github.com", arr.slice(19, 23)); - - if (user.passwordHash && !_isValidBase64(user.passwordHash)) { - return { - error: "Password hash should be base64 encoded.", - }; - } - if (user.salt && !_isValidBase64(user.salt)) { - return { - error: "Password salt should be base64 encoded.", - }; - } - return user; -}; - -var validateOptions = function (options) { - var hashOptions = _validateRequiredParameters(options); - if (!hashOptions.valid) { - return hashOptions; - } - var hashInputOrder = options.hashInputOrder ? options.hashInputOrder.toUpperCase() : undefined; - if (hashInputOrder) { - if (hashInputOrder != "SALT_FIRST" && hashInputOrder != "PASSWORD_FIRST") { - throw new FirebaseError("Unknown password hash order flag", { exit: 1 }); - } else { - hashOptions["passwordHashOrder"] = - hashInputOrder == "SALT_FIRST" ? "SALT_AND_PASSWORD" : "PASSWORD_AND_SALT"; - } - } - return hashOptions; -}; - -var _validateRequiredParameters = function (options) { - if (!options.hashAlgo) { - utils.logWarning("No hash algorithm specified. Password users cannot be imported."); - return { valid: true }; - } - var hashAlgo = options.hashAlgo.toUpperCase(); - let roundsNum; - switch (hashAlgo) { - case "HMAC_SHA512": - case "HMAC_SHA256": - case "HMAC_SHA1": - case "HMAC_MD5": - if (!options.hashKey || options.hashKey === "") { - throw new FirebaseError( - "Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo, - { exit: 1 } - ); - } - return { hashAlgo: hashAlgo, hashKey: options.hashKey, valid: true }; - case "MD5": - case "SHA1": - case "SHA256": - case "SHA512": - // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192] - roundsNum = parseInt(options.rounds, 10); - var minRounds = hashAlgo === "MD5" ? 0 : 1; - if (isNaN(roundsNum) || roundsNum < minRounds || roundsNum > 8192) { - throw new FirebaseError( - `Must provide valid rounds(${minRounds}..8192) for hash algorithm ${options.hashAlgo}`, - { exit: 1 } - ); - } - return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true }; - case "PBKDF_SHA1": - case "PBKDF2_SHA256": - roundsNum = parseInt(options.rounds, 10); - if (isNaN(roundsNum) || roundsNum < 0 || roundsNum > 120000) { - throw new FirebaseError( - "Must provide valid rounds(0..120000) for hash algorithm " + options.hashAlgo, - { exit: 1 } - ); - } - return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true }; - case "SCRYPT": - if (!options.hashKey || options.hashKey === "") { - throw new FirebaseError( - "Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo, - { exit: 1 } - ); - } - roundsNum = parseInt(options.rounds, 10); - if (isNaN(roundsNum) || roundsNum <= 0 || roundsNum > 8) { - throw new FirebaseError( - "Must provide valid rounds(1..8) for hash algorithm " + options.hashAlgo, - { exit: 1 } - ); - } - var memCost = parseInt(options.memCost, 10); - if (isNaN(memCost) || memCost <= 0 || memCost > 14) { - throw new FirebaseError( - "Must provide valid memory cost(1..14) for hash algorithm " + options.hashAlgo, - { exit: 1 } - ); - } - var saltSeparator = ""; - if (options.saltSeparator) { - saltSeparator = options.saltSeparator; - } - return { - hashAlgo: hashAlgo, - hashKey: options.hashKey, - saltSeparator: saltSeparator, - rounds: options.rounds, - memCost: options.memCost, - valid: true, - }; - case "BCRYPT": - return { hashAlgo: hashAlgo, valid: true }; - case "STANDARD_SCRYPT": - var cpuMemCost = parseInt(options.memCost, 10); - var parallelization = parseInt(options.parallelization, 10); - var blockSize = parseInt(options.blockSize, 10); - var dkLen = parseInt(options.dkLen, 10); - return { - hashAlgo: hashAlgo, - valid: true, - cpuMemCost: cpuMemCost, - parallelization: parallelization, - blockSize: blockSize, - dkLen: dkLen, - }; - default: - throw new FirebaseError("Unsupported hash algorithm " + clc.bold(options.hashAlgo)); - } -}; - -var _validateProviderUserInfo = function (providerUserInfo) { - if (!_.includes(ALLOWED_PROVIDER_IDS, providerUserInfo.providerId)) { - return { - error: JSON.stringify(providerUserInfo, null, 2) + " has unsupported providerId", - }; - } - var keydiff = _.difference(_.keys(providerUserInfo), ALLOWED_PROVIDER_USER_INFO_KEYS); - if (keydiff.length) { - return { - error: - JSON.stringify(providerUserInfo, null, 2) + " has unsupported keys: " + keydiff.join(","), - }; - } - return {}; -}; - -var validateUserJson = function (userJson) { - var keydiff = _.difference(_.keys(userJson), ALLOWED_JSON_KEYS); - if (keydiff.length) { - return { - error: JSON.stringify(userJson, null, 2) + " has unsupported keys: " + keydiff.join(","), - }; - } - if (userJson.providerUserInfo) { - for (var i = 0; i < userJson.providerUserInfo.length; i++) { - var res = _validateProviderUserInfo(userJson.providerUserInfo[i]); - if (res.error) { - return res; - } - } - } - var badFormat = JSON.stringify(userJson, null, 2) + " has invalid data format: "; - if (userJson.passwordHash && !_isValidBase64(userJson.passwordHash)) { - return { - error: badFormat + "Password hash should be base64 encoded.", - }; - } - if (userJson.salt && !_isValidBase64(userJson.salt)) { - return { - error: badFormat + "Password salt should be base64 encoded.", - }; - } - return {}; -}; - -var _sendRequest = function (projectId, userList, hashOptions) { - logger.info("Starting importing " + userList.length + " account(s)."); - return api - .request("POST", "/identitytoolkit/v3/relyingparty/uploadAccount", { - auth: true, - json: true, - data: _genUploadAccountPostBody(projectId, userList, hashOptions), - origin: api.googleOrigin, - }) - .then(function (ret) { - if (ret.body.error) { - logger.info("Encountered problems while importing accounts. Details:"); - logger.info( - ret.body.error.map(function (rawInfo) { - return { - account: JSON.stringify(userList[parseInt(rawInfo.index, 10)], null, 2), - reason: rawInfo.message, - }; - }) - ); - } else { - utils.logSuccess("Imported successfully."); - } - logger.info(); - }); -}; - -var serialImportUsers = function (projectId, hashOptions, userListArr, index) { - return _sendRequest(projectId, userListArr[index], hashOptions).then(function () { - if (index < userListArr.length - 1) { - return serialImportUsers(projectId, hashOptions, userListArr, index + 1); - } - }); -}; - -var accountImporter = { - validateOptions: validateOptions, - validateUserJson: validateUserJson, - transArrayToUser: transArrayToUser, - serialImportUsers: serialImportUsers, -}; - -module.exports = accountImporter; diff --git a/src/accountImporter.spec.ts b/src/accountImporter.spec.ts new file mode 100644 index 00000000000..bd8f7a792ba --- /dev/null +++ b/src/accountImporter.spec.ts @@ -0,0 +1,176 @@ +import * as nock from "nock"; +import { expect } from "chai"; + +import { googleOrigin } from "./api"; + +import * as accountImporter from "./accountImporter"; + +describe("accountImporter", () => { + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + + const transArrayToUser = accountImporter.transArrayToUser; + const validateOptions = accountImporter.validateOptions; + const validateUserJson = accountImporter.validateUserJson; + const serialImportUsers = accountImporter.serialImportUsers; + + describe("transArrayToUser", () => { + it("should reject when passwordHash is invalid base64", () => { + expect(transArrayToUser(["123", undefined, undefined, "false"])).to.have.property("error"); + }); + + it("should not reject when passwordHash is valid base64", () => { + expect( + transArrayToUser(["123", undefined, undefined, "Jlf7onfLbzqPNFP/1pqhx6fQF/w="]), + ).to.not.have.property("error"); + }); + }); + + describe("validateOptions", () => { + it("should reject when unsupported hash algorithm provided", () => { + expect(() => validateOptions({ hashAlgo: "MD2" })).to.throw(); + }); + + it("should reject when missing parameters", () => { + expect(() => validateOptions({ hashAlgo: "HMAC_SHA1" })).to.throw(); + }); + }); + + describe("validateUserJson", () => { + it("should reject when unknown fields in user json", () => { + expect( + validateUserJson({ + uid: "123", + email: "test@test.org", + }), + ).to.have.property("error"); + }); + + it("should reject when unknown fields in providerUserInfo of user json", () => { + expect( + validateUserJson({ + localId: "123", + email: "test@test.org", + providerUserInfo: [ + { + providerId: "google.com", + googleId: "abc", + email: "test@test.org", + }, + ], + }), + ).to.have.property("error"); + }); + + it("should reject when unknown providerUserInfo of user json", () => { + expect( + validateUserJson({ + localId: "123", + email: "test@test.org", + providerUserInfo: [ + { + providerId: "otheridp.com", + rawId: "abc", + email: "test@test.org", + }, + ], + }), + ).to.have.property("error"); + }); + + it("should reject when passwordHash is invalid base64", () => { + expect( + validateUserJson({ + localId: "123", + passwordHash: "false", + }), + ).to.have.property("error"); + }); + + it("should not reject when passwordHash is valid base64", () => { + expect( + validateUserJson({ + localId: "123", + passwordHash: "Jlf7onfLbzqPNFP/1pqhx6fQF/w=", + }), + ).to.not.have.property("error"); + }); + }); + + describe("serialImportUsers", () => { + let batches: { localId: string; email: string }[][] = []; + const hashOptions = { + hashAlgo: "HMAC_SHA1", + hashKey: "a2V5MTIz", + }; + let expectedResponse: { status: number; body: any }[] = []; + + beforeEach(() => { + for (let i = 0; i < 10; i++) { + batches.push([ + { + localId: i.toString(), + email: `test${i}@test.org`, + }, + ]); + expectedResponse.push({ + status: 200, + body: {}, + }); + } + }); + + afterEach(() => { + batches = []; + expectedResponse = []; + }); + + it("should call api.request multiple times", async () => { + for (let i = 0; i < batches.length; i++) { + nock(googleOrigin()) + .post("/identitytoolkit/v3/relyingparty/uploadAccount", { + hashAlgorithm: "HMAC_SHA1", + signerKey: "a2V5MTIz", + targetProjectId: "test-project-id", + users: [{ email: `test${i}@test.org`, localId: i.toString() }], + }) + .once() + .reply(expectedResponse[i].status, expectedResponse[i].body); + } + await serialImportUsers("test-project-id", hashOptions, batches, 0); + expect(nock.isDone()).to.be.true; + }); + + it("should continue when some request's response is 200 but has `error` in response", async () => { + expectedResponse[5] = { + status: 200, + body: { + error: [ + { + index: 0, + message: "some error message", + }, + ], + }, + }; + for (let i = 0; i < batches.length; i++) { + nock(googleOrigin()) + .post("/identitytoolkit/v3/relyingparty/uploadAccount", { + hashAlgorithm: "HMAC_SHA1", + signerKey: "a2V5MTIz", + targetProjectId: "test-project-id", + users: [{ email: `test${i}@test.org`, localId: i.toString() }], + }) + .once() + .reply(expectedResponse[i].status, expectedResponse[i].body); + } + await serialImportUsers("test-project-id", hashOptions, batches, 0); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/accountImporter.ts b/src/accountImporter.ts new file mode 100644 index 00000000000..fc207b66626 --- /dev/null +++ b/src/accountImporter.ts @@ -0,0 +1,339 @@ +import * as clc from "colorette"; + +import { Client } from "./apiv2"; +import { googleOrigin } from "./api"; +import { logger } from "./logger"; +import { FirebaseError } from "./error"; +import * as utils from "./utils"; + +const apiClient = new Client({ + urlPrefix: googleOrigin(), +}); + +// TODO: support for MFA at runtime was added in PR #3173, but this importer currently ignores `mfaInfo` and loses the data on import. +const ALLOWED_JSON_KEYS = [ + "localId", + "email", + "emailVerified", + "passwordHash", + "salt", + "displayName", + "photoUrl", + "createdAt", + "lastSignedInAt", + "providerUserInfo", + "phoneNumber", + "disabled", + "customAttributes", +]; +const ALLOWED_JSON_KEYS_RENAMING = { + lastSignedInAt: "lastLoginAt", +}; +const ALLOWED_PROVIDER_USER_INFO_KEYS = ["providerId", "rawId", "email", "displayName", "photoUrl"]; +const ALLOWED_PROVIDER_IDS = ["google.com", "facebook.com", "twitter.com", "github.com"]; + +function isValidBase64(str: string): boolean { + const expected = Buffer.from(str, "base64").toString("base64"); + // Buffer automatically pads with '=' character, + // but input string might not have padding. + if (str.length < expected.length && !str.endsWith("=")) { + str += "=".repeat(expected.length - str.length); + } + return expected === str; +} + +function toWebSafeBase64(data: string): string { + return data.replace(/\//g, "_").replace(/\+/g, "-"); +} + +function addProviderUserInfo(user: any, providerId: string, arr: any[]) { + if (arr[0]) { + user.providerUserInfo.push({ + providerId: providerId, + rawId: arr[0], + email: arr[1], + displayName: arr[2], + photoUrl: arr[3], + }); + } +} + +function genUploadAccountPostBody(projectId: string, accounts: any[], hashOptions: any) { + const postBody: any = { + users: accounts.map((account) => { + if (account.passwordHash) { + account.passwordHash = toWebSafeBase64(account.passwordHash); + } + if (account.salt) { + account.salt = toWebSafeBase64(account.salt); + } + for (const [key, value] of Object.entries(ALLOWED_JSON_KEYS_RENAMING)) { + if (account[key]) { + account[value] = account[key]; + delete account[key]; + } + } + return account; + }), + }; + if (hashOptions.hashAlgo) { + postBody.hashAlgorithm = hashOptions.hashAlgo; + } + if (hashOptions.hashKey) { + postBody.signerKey = toWebSafeBase64(hashOptions.hashKey); + } + if (hashOptions.saltSeparator) { + postBody.saltSeparator = toWebSafeBase64(hashOptions.saltSeparator); + } + if (hashOptions.rounds) { + postBody.rounds = hashOptions.rounds; + } + if (hashOptions.memCost) { + postBody.memoryCost = hashOptions.memCost; + } + if (hashOptions.cpuMemCost) { + postBody.cpuMemCost = hashOptions.cpuMemCost; + } + if (hashOptions.parallelization) { + postBody.parallelization = hashOptions.parallelization; + } + if (hashOptions.blockSize) { + postBody.blockSize = hashOptions.blockSize; + } + if (hashOptions.dkLen) { + postBody.dkLen = hashOptions.dkLen; + } + if (hashOptions.passwordHashOrder) { + postBody.passwordHashOrder = hashOptions.passwordHashOrder; + } + postBody.targetProjectId = projectId; + return postBody; +} + +export function transArrayToUser(arr: any[]): any { + const user = { + localId: arr[0], + email: arr[1], + emailVerified: arr[2] === "true", + passwordHash: arr[3], + salt: arr[4], + displayName: arr[5], + photoUrl: arr[6], + createdAt: arr[23], + lastLoginAt: arr[24], + phoneNumber: arr[25], + providerUserInfo: [], + disabled: arr[26], + customAttributes: arr[27], + }; + addProviderUserInfo(user, "google.com", arr.slice(7, 11)); + addProviderUserInfo(user, "facebook.com", arr.slice(11, 15)); + addProviderUserInfo(user, "twitter.com", arr.slice(15, 19)); + addProviderUserInfo(user, "github.com", arr.slice(19, 23)); + + if (user.passwordHash && !isValidBase64(user.passwordHash)) { + return { + error: "Password hash should be base64 encoded.", + }; + } + if (user.salt && !isValidBase64(user.salt)) { + return { + error: "Password salt should be base64 encoded.", + }; + } + return user; +} + +export function validateOptions(options: any): any { + const hashOptions = validateRequiredParameters(options); + if (!hashOptions.valid) { + return hashOptions; + } + const hashInputOrder = options.hashInputOrder ? options.hashInputOrder.toUpperCase() : undefined; + if (hashInputOrder) { + if (hashInputOrder !== "SALT_FIRST" && hashInputOrder !== "PASSWORD_FIRST") { + throw new FirebaseError("Unknown password hash order flag"); + } else { + hashOptions["passwordHashOrder"] = + hashInputOrder === "SALT_FIRST" ? "SALT_AND_PASSWORD" : "PASSWORD_AND_SALT"; + } + } + return hashOptions; +} + +function validateRequiredParameters(options: any): any { + if (!options.hashAlgo) { + utils.logWarning("No hash algorithm specified. Password users cannot be imported."); + return { valid: true }; + } + const hashAlgo = options.hashAlgo.toUpperCase(); + let roundsNum; + switch (hashAlgo) { + case "HMAC_SHA512": + case "HMAC_SHA256": + case "HMAC_SHA1": + case "HMAC_MD5": + if (!options.hashKey || options.hashKey === "") { + throw new FirebaseError( + "Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo, + ); + } + return { hashAlgo: hashAlgo, hashKey: options.hashKey, valid: true }; + case "MD5": + case "SHA1": + case "SHA256": + case "SHA512": + // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192] + roundsNum = parseInt(options.rounds, 10); + const minRounds = hashAlgo === "MD5" ? 0 : 1; + if (isNaN(roundsNum) || roundsNum < minRounds || roundsNum > 8192) { + throw new FirebaseError( + `Must provide valid rounds(${minRounds}..8192) for hash algorithm ${options.hashAlgo}`, + ); + } + return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true }; + case "PBKDF_SHA1": + case "PBKDF2_SHA256": + roundsNum = parseInt(options.rounds, 10); + if (isNaN(roundsNum) || roundsNum < 0 || roundsNum > 120000) { + throw new FirebaseError( + "Must provide valid rounds(0..120000) for hash algorithm " + options.hashAlgo, + ); + } + return { hashAlgo: hashAlgo, rounds: options.rounds, valid: true }; + case "SCRYPT": + if (!options.hashKey || options.hashKey === "") { + throw new FirebaseError( + "Must provide hash key(base64 encoded) for hash algorithm " + options.hashAlgo, + ); + } + roundsNum = parseInt(options.rounds, 10); + if (isNaN(roundsNum) || roundsNum <= 0 || roundsNum > 8) { + throw new FirebaseError( + "Must provide valid rounds(1..8) for hash algorithm " + options.hashAlgo, + ); + } + const memCost = parseInt(options.memCost, 10); + if (isNaN(memCost) || memCost <= 0 || memCost > 14) { + throw new FirebaseError( + "Must provide valid memory cost(1..14) for hash algorithm " + options.hashAlgo, + ); + } + let saltSeparator = ""; + if (options.saltSeparator) { + saltSeparator = options.saltSeparator; + } + return { + hashAlgo: hashAlgo, + hashKey: options.hashKey, + saltSeparator: saltSeparator, + rounds: options.rounds, + memCost: options.memCost, + valid: true, + }; + case "BCRYPT": + return { hashAlgo: hashAlgo, valid: true }; + case "STANDARD_SCRYPT": + const cpuMemCost = parseInt(options.memCost, 10); + const parallelization = parseInt(options.parallelization, 10); + const blockSize = parseInt(options.blockSize, 10); + const dkLen = parseInt(options.dkLen, 10); + return { + hashAlgo: hashAlgo, + valid: true, + cpuMemCost: cpuMemCost, + parallelization: parallelization, + blockSize: blockSize, + dkLen: dkLen, + }; + default: + throw new FirebaseError("Unsupported hash algorithm " + clc.bold(options.hashAlgo)); + } +} + +function validateProviderUserInfo(providerUserInfo: { providerId: string; error?: string }): { + error?: string; +} { + if (!ALLOWED_PROVIDER_IDS.includes(providerUserInfo.providerId)) { + return { + error: JSON.stringify(providerUserInfo, null, 2) + " has unsupported providerId", + }; + } + const keydiff = Object.keys(providerUserInfo).filter( + (k) => !ALLOWED_PROVIDER_USER_INFO_KEYS.includes(k), + ); + if (keydiff.length) { + return { + error: + JSON.stringify(providerUserInfo, null, 2) + " has unsupported keys: " + keydiff.join(","), + }; + } + return {}; +} + +export function validateUserJson(userJson: any): { error?: string } { + const keydiff = Object.keys(userJson).filter((k) => !ALLOWED_JSON_KEYS.includes(k)); + if (keydiff.length) { + return { + error: JSON.stringify(userJson, null, 2) + " has unsupported keys: " + keydiff.join(","), + }; + } + if (userJson.providerUserInfo) { + for (let i = 0; i < userJson.providerUserInfo.length; i++) { + const res = validateProviderUserInfo(userJson.providerUserInfo[i]); + if (res.error) { + return res; + } + } + } + const badFormat = JSON.stringify(userJson, null, 2) + " has invalid data format: "; + if (userJson.passwordHash && !isValidBase64(userJson.passwordHash)) { + return { + error: badFormat + "Password hash should be base64 encoded.", + }; + } + if (userJson.salt && !isValidBase64(userJson.salt)) { + return { + error: badFormat + "Password salt should be base64 encoded.", + }; + } + return {}; +} + +async function sendRequest(projectId: string, userList: any[], hashOptions: any): Promise { + logger.info("Starting importing " + userList.length + " account(s)."); + const postData = genUploadAccountPostBody(projectId, userList, hashOptions); + return apiClient + .post("/identitytoolkit/v3/relyingparty/uploadAccount", postData, { + skipLog: { body: true }, // Contains a lot of PII - don't log. + }) + .then((ret) => { + if (ret.body.error) { + logger.info("Encountered problems while importing accounts. Details:"); + logger.info( + ret.body.error.map((rawInfo: any) => { + return { + account: JSON.stringify(userList[parseInt(rawInfo.index, 10)], null, 2), + reason: rawInfo.message, + }; + }), + ); + } else { + utils.logSuccess("Imported successfully."); + } + logger.info(); + }); +} + +export function serialImportUsers( + projectId: string, + hashOptions: any, + userListArr: any[], + index: number, +): Promise { + return sendRequest(projectId, userListArr[index], hashOptions).then(() => { + if (index < userListArr.length - 1) { + return serialImportUsers(projectId, hashOptions, userListArr, index + 1); + } + }); +} diff --git a/src/api.js b/src/api.js deleted file mode 100644 index ef40a7f190b..00000000000 --- a/src/api.js +++ /dev/null @@ -1,346 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var querystring = require("querystring"); -var request = require("request"); -var url = require("url"); - -var { Constants } = require("./emulator/constants"); -var { FirebaseError } = require("./error"); -const { logger } = require("./logger"); -var responseToError = require("./responseToError"); -var scopes = require("./scopes"); -var utils = require("./utils"); -var CLI_VERSION = require("../package.json").version; - -var accessToken; -var refreshToken; -var commandScopes; - -var _request = function (options, logOptions) { - logOptions = logOptions || {}; - var qsLog = ""; - var bodyLog = ""; - - if (options.qs && !logOptions.skipQueryParams) { - qsLog = JSON.stringify(options.qs); - } - - if (!logOptions.skipRequestBody) { - bodyLog = options.body || options.form || ""; - } - - logger.debug(">>> HTTP REQUEST", options.method, options.url, qsLog, "\n", bodyLog); - - options.headers = options.headers || {}; - options.headers["connection"] = "keep-alive"; - - return new Promise(function (resolve, reject) { - var req = request(options, function (err, response, body) { - if (err) { - return reject( - new FirebaseError("Server Error. " + err.message, { - original: err, - exit: 2, - }) - ); - } - - logger.debug("<<< HTTP RESPONSE", response.statusCode, response.headers); - - if (response.statusCode >= 400 && !logOptions.skipResponseBody) { - logger.debug("<<< HTTP RESPONSE BODY", response.body); - if (!options.resolveOnHTTPError) { - return reject(responseToError(response, body)); - } - } - - return resolve({ - status: response.statusCode, - response: response, - body: body, - }); - }); - - if (_.size(options.files) > 0) { - var form = req.form(); - _.forEach(options.files, function (details, param) { - form.append(param, details.stream, { - knownLength: details.knownLength, - filename: details.filename, - contentType: details.contentType, - }); - }); - } - }); -}; - -var _appendQueryData = function (path, data) { - if (data && _.size(data) > 0) { - path += _.includes(path, "?") ? "&" : "?"; - path += querystring.stringify(data); - } - return path; -}; - -var api = { - // "In this context, the client secret is obviously not treated as a secret" - // https://developers.google.com/identity/protocols/OAuth2InstalledApp - clientId: utils.envOverride( - "FIREBASE_CLIENT_ID", - "563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com" - ), - clientSecret: utils.envOverride("FIREBASE_CLIENT_SECRET", "j9iVZfS8kkCEFUPaAeJV0sAi"), - cloudbillingOrigin: utils.envOverride( - "FIREBASE_CLOUDBILLING_URL", - "https://cloudbilling.googleapis.com" - ), - cloudloggingOrigin: utils.envOverride( - "FIREBASE_CLOUDLOGGING_URL", - "https://logging.googleapis.com" - ), - containerRegistryDomain: utils.envOverride("CONTAINER_REGISTRY_DOMAIN", "gcr.io"), - artifactRegistryDomain: utils.envOverride( - "ARTIFACT_REGISTRY_DOMAIN", - "https://artifactregistry.googleapis.com" - ), - appDistributionOrigin: utils.envOverride( - "FIREBASE_APP_DISTRIBUTION_URL", - "https://firebaseappdistribution.googleapis.com" - ), - appengineOrigin: utils.envOverride("FIREBASE_APPENGINE_URL", "https://appengine.googleapis.com"), - authOrigin: utils.envOverride("FIREBASE_AUTH_URL", "https://accounts.google.com"), - consoleOrigin: utils.envOverride("FIREBASE_CONSOLE_URL", "https://console.firebase.google.com"), - deployOrigin: utils.envOverride( - "FIREBASE_DEPLOY_URL", - utils.envOverride("FIREBASE_UPLOAD_URL", "https://deploy.firebase.com") - ), - dynamicLinksOrigin: utils.envOverride( - "FIREBASE_DYNAMIC_LINKS_URL", - "https://firebasedynamiclinks.googleapis.com" - ), - dynamicLinksKey: utils.envOverride( - "FIREBASE_DYNAMIC_LINKS_KEY", - "AIzaSyB6PtY5vuiSB8MNgt20mQffkOlunZnHYiQ" - ), - firebaseApiOrigin: utils.envOverride("FIREBASE_API_URL", "https://firebase.googleapis.com"), - firebaseExtensionsRegistryOrigin: utils.envOverride( - "FIREBASE_EXT_REGISTRY_ORIGIN", - "https://extensions-registry.firebaseapp.com" - ), - firedataOrigin: utils.envOverride("FIREBASE_FIREDATA_URL", "https://mobilesdk-pa.googleapis.com"), - firestoreOriginOrEmulator: utils.envOverride( - Constants.FIRESTORE_EMULATOR_HOST, - utils.envOverride("FIRESTORE_URL", "https://firestore.googleapis.com"), - (val) => { - if (val.startsWith("http")) { - return val; - } - return `http://${val}`; - } - ), - firestoreOrigin: utils.envOverride("FIRESTORE_URL", "https://firestore.googleapis.com"), - functionsOrigin: utils.envOverride( - "FIREBASE_FUNCTIONS_URL", - "https://cloudfunctions.googleapis.com" - ), - functionsV2Origin: utils.envOverride( - "FIREBASE_FUNCTIONS_V2_URL", - "https://cloudfunctions.googleapis.com" - ), - runOrigin: utils.envOverride("CLOUD_RUN_URL", "https://run.googleapis.com"), - functionsDefaultRegion: utils.envOverride("FIREBASE_FUNCTIONS_DEFAULT_REGION", "us-central1"), - cloudschedulerOrigin: utils.envOverride( - "FIREBASE_CLOUDSCHEDULER_URL", - "https://cloudscheduler.googleapis.com" - ), - cloudTasksOrigin: utils.envOverride( - "FIREBASE_CLOUD_TAKS_URL", - "https://cloudtasks.googleapis.com" - ), - pubsubOrigin: utils.envOverride("FIREBASE_PUBSUB_URL", "https://pubsub.googleapis.com"), - googleOrigin: utils.envOverride( - "FIREBASE_TOKEN_URL", - utils.envOverride("FIREBASE_GOOGLE_URL", "https://www.googleapis.com") - ), - hostingOrigin: utils.envOverride("FIREBASE_HOSTING_URL", "https://web.app"), - identityOrigin: utils.envOverride( - "FIREBASE_IDENTITY_URL", - "https://identitytoolkit.googleapis.com" - ), - iamOrigin: utils.envOverride("FIREBASE_IAM_URL", "https://iam.googleapis.com"), - extensionsOrigin: utils.envOverride( - "FIREBASE_EXT_URL", - "https://firebaseextensions.googleapis.com" - ), - realtimeOrigin: utils.envOverride("FIREBASE_REALTIME_URL", "https://firebaseio.com"), - rtdbManagementOrigin: utils.envOverride( - "FIREBASE_RTDB_MANAGEMENT_URL", - "https://firebasedatabase.googleapis.com" - ), - rtdbMetadataOrigin: utils.envOverride( - "FIREBASE_RTDB_METADATA_URL", - "https://metadata-dot-firebase-prod.appspot.com" - ), - remoteConfigApiOrigin: utils.envOverride( - "FIREBASE_REMOTE_CONFIG_URL", - "https://firebaseremoteconfig.googleapis.com" - ), - resourceManagerOrigin: utils.envOverride( - "FIREBASE_RESOURCEMANAGER_URL", - "https://cloudresourcemanager.googleapis.com" - ), - rulesOrigin: utils.envOverride("FIREBASE_RULES_URL", "https://firebaserules.googleapis.com"), - runtimeconfigOrigin: utils.envOverride( - "FIREBASE_RUNTIMECONFIG_URL", - "https://runtimeconfig.googleapis.com" - ), - storageOrigin: utils.envOverride("FIREBASE_STORAGE_URL", "https://storage.googleapis.com"), - firebaseStorageOrigin: utils.envOverride( - "FIREBASE_FIREBASESTORAGE_URL", - "https://firebasestorage.googleapis.com" - ), - hostingApiOrigin: utils.envOverride( - "FIREBASE_HOSTING_API_URL", - "https://firebasehosting.googleapis.com" - ), - cloudRunApiOrigin: utils.envOverride("CLOUD_RUN_API_URL", "https://run.googleapis.com"), - serviceUsageOrigin: utils.envOverride( - "FIREBASE_SERVICE_USAGE_URL", - "https://serviceusage.googleapis.com" - ), - githubOrigin: utils.envOverride("GITHUB_URL", "https://github.com"), - githubApiOrigin: utils.envOverride("GITHUB_API_URL", "https://api.github.com"), - secretManagerOrigin: utils.envOverride( - "CLOUD_SECRET_MANAGER_URL", - "https://secretmanager.googleapis.com" - ), - githubClientId: utils.envOverride("GITHUB_CLIENT_ID", "89cf50f02ac6aaed3484"), - githubClientSecret: utils.envOverride( - "GITHUB_CLIENT_SECRET", - "3330d14abc895d9a74d5f17cd7a00711fa2c5bf0" - ), - setRefreshToken: function (token) { - refreshToken = token; - }, - setAccessToken: function (token) { - accessToken = token; - }, - getScopes: function () { - return commandScopes; - }, - setScopes: function (s) { - commandScopes = _.uniq( - _.flatten( - [ - scopes.EMAIL, - scopes.OPENID, - scopes.CLOUD_PROJECTS_READONLY, - scopes.FIREBASE_PLATFORM, - ].concat(s || []) - ) - ); - logger.debug("> command requires scopes:", JSON.stringify(commandScopes)); - }, - getAccessToken: function () { - // Runtime fetch of Auth singleton to prevent circular module dependencies - return accessToken - ? Promise.resolve({ access_token: accessToken }) - : require("./auth").getAccessToken(refreshToken, commandScopes); - }, - addRequestHeaders: function (reqOptions, options) { - _.set(reqOptions, ["headers", "User-Agent"], "FirebaseCLI/" + CLI_VERSION); - _.set(reqOptions, ["headers", "X-Client-Version"], "FirebaseCLI/" + CLI_VERSION); - - var secureRequest = true; - if (options && options.origin) { - // Only 'https' requests are secure. Protocol includes the final ':' - // https://developer.mozilla.org/en-US/docs/Web/API/URL/protocol - const originUrl = url.parse(options.origin); - secureRequest = originUrl.protocol === "https:"; - } - - // For insecure requests we send a special 'owner" token which the emulators - // will accept and other secure APIs will deny. - var getTokenPromise = secureRequest - ? api.getAccessToken() - : Promise.resolve({ access_token: "owner" }); - - return getTokenPromise.then(function (result) { - _.set(reqOptions, "headers.authorization", "Bearer " + result.access_token); - return reqOptions; - }); - }, - request: function (method, resource, options) { - options = _.extend( - { - data: {}, - resolveOnHTTPError: false, // by default, status codes >= 400 leads to reject - json: true, - }, - options - ); - - if (!options.origin) { - throw new FirebaseError("Cannot make request without an origin", { exit: 2 }); - } - - var validMethods = ["GET", "PUT", "POST", "DELETE", "PATCH"]; - - if (validMethods.indexOf(method) < 0) { - method = "GET"; - } - - var reqOptions = { - method: method, - }; - - if (options.query) { - resource = _appendQueryData(resource, options.query); - } - - if (method === "GET") { - resource = _appendQueryData(resource, options.data); - } else { - if (_.size(options.data) > 0) { - reqOptions.body = options.data; - } else if (_.size(options.form) > 0) { - reqOptions.form = options.form; - } - } - - reqOptions.url = options.origin + resource; - reqOptions.files = options.files; - reqOptions.resolveOnHTTPError = options.resolveOnHTTPError; - reqOptions.json = options.json; - reqOptions.qs = options.qs; - reqOptions.headers = options.headers; - reqOptions.timeout = options.timeout; - - var requestFunction = function () { - return _request(reqOptions, options.logOptions); - }; - - if (options.auth === true) { - requestFunction = function () { - return api.addRequestHeaders(reqOptions, options).then(function (reqOptionsWithToken) { - return _request(reqOptionsWithToken, options.logOptions); - }); - }; - } - - return requestFunction().catch(function (err) { - if ( - options.retryCodes && - _.includes(options.retryCodes, _.get(err, "context.response.statusCode")) - ) { - return new Promise(function (resolve) { - setTimeout(resolve, 1000); - }).then(requestFunction); - } - return Promise.reject(err); - }); - }, -}; - -module.exports = api; diff --git a/src/api.spec.ts b/src/api.spec.ts new file mode 100644 index 00000000000..bf647849330 --- /dev/null +++ b/src/api.spec.ts @@ -0,0 +1,38 @@ +import { expect } from "chai"; + +import * as utils from "./utils"; + +describe("api", () => { + beforeEach(() => { + // The api module resolves env var statically so we need to + // do lazy imports and clear the import each time. + delete require.cache[require.resolve("./api")]; + }); + + afterEach(() => { + delete process.env.FIRESTORE_EMULATOR_HOST; + delete process.env.FIRESTORE_URL; + + // This is dirty, but utils keeps stateful overrides and we need to clear it + utils.envOverrides.length = 0; + }); + + after(() => { + delete require.cache[require.resolve("./api")]; + }); + + it("should override with FIRESTORE_URL", () => { + process.env.FIRESTORE_URL = "http://foobar.com"; + + const api = require("./api"); + expect(api.firestoreOrigin()).to.eq("http://foobar.com"); + }); + + it("should prefer FIRESTORE_EMULATOR_HOST to FIRESTORE_URL", () => { + process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080"; + process.env.FIRESTORE_URL = "http://foobar.com"; + + const api = require("./api"); + expect(api.firestoreOriginOrEmulator()).to.eq("http://localhost:8080"); + }); +}); diff --git a/src/api.ts b/src/api.ts new file mode 100755 index 00000000000..31913262fa8 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,173 @@ +import { Constants } from "./emulator/constants"; +import { logger } from "./logger"; +import * as scopes from "./scopes"; +import * as utils from "./utils"; + +let commandScopes = new Set(); + +export const authProxyOrigin = () => + utils.envOverride("FIREBASE_AUTHPROXY_URL", "https://auth.firebase.tools"); +// "In this context, the client secret is obviously not treated as a secret" +// https://developers.google.com/identity/protocols/OAuth2InstalledApp +export const clientId = () => + utils.envOverride( + "FIREBASE_CLIENT_ID", + "563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com", + ); +export const clientSecret = () => + utils.envOverride("FIREBASE_CLIENT_SECRET", "j9iVZfS8kkCEFUPaAeJV0sAi"); +export const cloudbillingOrigin = () => + utils.envOverride("FIREBASE_CLOUDBILLING_URL", "https://cloudbilling.googleapis.com"); +export const cloudloggingOrigin = () => + utils.envOverride("FIREBASE_CLOUDLOGGING_URL", "https://logging.googleapis.com"); +export const cloudMonitoringOrigin = () => + utils.envOverride("CLOUD_MONITORING_URL", "https://monitoring.googleapis.com"); +export const containerRegistryDomain = () => + utils.envOverride("CONTAINER_REGISTRY_DOMAIN", "gcr.io"); + +export const developerConnectOrigin = () => + utils.envOverride("DEVELOPERCONNECT_URL", "https://developerconnect.googleapis.com"); +export const developerConnectP4SADomain = () => + utils.envOverride("DEVELOPERCONNECT_P4SA_DOMAIN", "gcp-sa-devconnect.iam.gserviceaccount.com"); + +export const artifactRegistryDomain = () => + utils.envOverride("ARTIFACT_REGISTRY_DOMAIN", "https://artifactregistry.googleapis.com"); +export const appDistributionOrigin = () => + utils.envOverride( + "FIREBASE_APP_DISTRIBUTION_URL", + "https://firebaseappdistribution.googleapis.com", + ); +export const apphostingOrigin = () => + utils.envOverride("FIREBASE_APPHOSTING_URL", "https://firebaseapphosting.googleapis.com"); +export const apphostingP4SADomain = () => + utils.envOverride( + "FIREBASE_APPHOSTING_P4SA_DOMAIN", + "gcp-sa-firebaseapphosting.iam.gserviceaccount.com", + ); + +export const authOrigin = () => + utils.envOverride("FIREBASE_AUTH_URL", "https://accounts.google.com"); +export const consoleOrigin = () => + utils.envOverride("FIREBASE_CONSOLE_URL", "https://console.firebase.google.com"); +export const dynamicLinksOrigin = () => + utils.envOverride("FIREBASE_DYNAMIC_LINKS_URL", "https://firebasedynamiclinks.googleapis.com"); +export const dynamicLinksKey = () => + utils.envOverride("FIREBASE_DYNAMIC_LINKS_KEY", "AIzaSyB6PtY5vuiSB8MNgt20mQffkOlunZnHYiQ"); +export const eventarcOrigin = () => + utils.envOverride("EVENTARC_URL", "https://eventarc.googleapis.com"); +export const firebaseApiOrigin = () => + utils.envOverride("FIREBASE_API_URL", "https://firebase.googleapis.com"); +export const firebaseExtensionsRegistryOrigin = () => + utils.envOverride("FIREBASE_EXT_REGISTRY_ORIGIN", "https://extensions-registry.firebaseapp.com"); +export const firedataOrigin = () => + utils.envOverride("FIREBASE_FIREDATA_URL", "https://mobilesdk-pa.googleapis.com"); +export const firestoreOriginOrEmulator = () => + utils.envOverride( + Constants.FIRESTORE_EMULATOR_HOST, + utils.envOverride("FIRESTORE_URL", "https://firestore.googleapis.com"), + (val) => { + if (val.startsWith("http")) { + return val; + } + return `http://${val}`; + }, + ); +export const firestoreOrigin = () => + utils.envOverride("FIRESTORE_URL", "https://firestore.googleapis.com"); +export const functionsOrigin = () => + utils.envOverride("FIREBASE_FUNCTIONS_URL", "https://cloudfunctions.googleapis.com"); +export const functionsV2Origin = () => + utils.envOverride("FIREBASE_FUNCTIONS_V2_URL", "https://cloudfunctions.googleapis.com"); +export const runOrigin = () => utils.envOverride("CLOUD_RUN_URL", "https://run.googleapis.com"); +export const functionsDefaultRegion = () => + utils.envOverride("FIREBASE_FUNCTIONS_DEFAULT_REGION", "us-central1"); + +export const cloudbuildOrigin = () => + utils.envOverride("FIREBASE_CLOUDBUILD_URL", "https://cloudbuild.googleapis.com"); + +export const cloudschedulerOrigin = () => + utils.envOverride("FIREBASE_CLOUDSCHEDULER_URL", "https://cloudscheduler.googleapis.com"); +export const cloudTasksOrigin = () => + utils.envOverride("FIREBASE_CLOUD_TAKS_URL", "https://cloudtasks.googleapis.com"); +export const pubsubOrigin = () => + utils.envOverride("FIREBASE_PUBSUB_URL", "https://pubsub.googleapis.com"); +export const googleOrigin = () => + utils.envOverride( + "FIREBASE_TOKEN_URL", + utils.envOverride("FIREBASE_GOOGLE_URL", "https://www.googleapis.com"), + ); +export const hostingOrigin = () => utils.envOverride("FIREBASE_HOSTING_URL", "https://web.app"); +export const identityOrigin = () => + utils.envOverride("FIREBASE_IDENTITY_URL", "https://identitytoolkit.googleapis.com"); +export const iamOrigin = () => utils.envOverride("FIREBASE_IAM_URL", "https://iam.googleapis.com"); +export const extensionsOrigin = () => + utils.envOverride("FIREBASE_EXT_URL", "https://firebaseextensions.googleapis.com"); +export const extensionsPublisherOrigin = () => + utils.envOverride( + "FIREBASE_EXT_PUBLISHER_URL", + "https://firebaseextensionspublisher.googleapis.com", + ); +export const extensionsTOSOrigin = () => + utils.envOverride("FIREBASE_EXT_TOS_URL", "https://firebaseextensionstos-pa.googleapis.com"); +export const realtimeOrigin = () => + utils.envOverride("FIREBASE_REALTIME_URL", "https://firebaseio.com"); +export const rtdbManagementOrigin = () => + utils.envOverride("FIREBASE_RTDB_MANAGEMENT_URL", "https://firebasedatabase.googleapis.com"); +export const rtdbMetadataOrigin = () => + utils.envOverride("FIREBASE_RTDB_METADATA_URL", "https://metadata-dot-firebase-prod.appspot.com"); +export const remoteConfigApiOrigin = () => + utils.envOverride("FIREBASE_REMOTE_CONFIG_URL", "https://firebaseremoteconfig.googleapis.com"); +export const resourceManagerOrigin = () => + utils.envOverride("FIREBASE_RESOURCEMANAGER_URL", "https://cloudresourcemanager.googleapis.com"); +export const rulesOrigin = () => + utils.envOverride("FIREBASE_RULES_URL", "https://firebaserules.googleapis.com"); +export const runtimeconfigOrigin = () => + utils.envOverride("FIREBASE_RUNTIMECONFIG_URL", "https://runtimeconfig.googleapis.com"); +export const storageOrigin = () => + utils.envOverride("FIREBASE_STORAGE_URL", "https://storage.googleapis.com"); +export const firebaseStorageOrigin = () => + utils.envOverride("FIREBASE_FIREBASESTORAGE_URL", "https://firebasestorage.googleapis.com"); +export const hostingApiOrigin = () => + utils.envOverride("FIREBASE_HOSTING_API_URL", "https://firebasehosting.googleapis.com"); +export const cloudRunApiOrigin = () => + utils.envOverride("CLOUD_RUN_API_URL", "https://run.googleapis.com"); +export const serviceUsageOrigin = () => + utils.envOverride("FIREBASE_SERVICE_USAGE_URL", "https://serviceusage.googleapis.com"); + +export const githubOrigin = () => utils.envOverride("GITHUB_URL", "https://github.com"); +export const githubApiOrigin = () => utils.envOverride("GITHUB_API_URL", "https://api.github.com"); +export const secretManagerOrigin = () => + utils.envOverride("CLOUD_SECRET_MANAGER_URL", "https://secretmanager.googleapis.com"); +export const computeOrigin = () => + utils.envOverride("COMPUTE_URL", "https://compute.googleapis.com"); +export const githubClientId = () => utils.envOverride("GITHUB_CLIENT_ID", "89cf50f02ac6aaed3484"); +export const githubClientSecret = () => + utils.envOverride("GITHUB_CLIENT_SECRET", "3330d14abc895d9a74d5f17cd7a00711fa2c5bf0"); + +export const dataconnectOrigin = () => + utils.envOverride("FIREBASE_DATACONNECT_URL", "https://firebasedataconnect.googleapis.com"); +export const dataConnectLocalConnString = () => + utils.envOverride("FIREBASE_DATACONNECT_POSTGRESQL_STRING", ""); +export const cloudSQLAdminOrigin = () => + utils.envOverride("CLOUD_SQL_URL", "https://sqladmin.googleapis.com"); +export const vertexAIOrigin = () => + utils.envOverride("VERTEX_AI_URL", "https://aiplatform.googleapis.com"); + +/** Gets scopes that have been set. */ +export function getScopes(): string[] { + return Array.from(commandScopes); +} + +/** Sets scopes for API calls. */ +export function setScopes(sps: string[] = []): void { + commandScopes = new Set([ + scopes.EMAIL, + scopes.OPENID, + scopes.CLOUD_PROJECTS_READONLY, + scopes.FIREBASE_PLATFORM, + ]); + for (const s of sps) { + commandScopes.add(s); + } + logger.debug("> command requires scopes:", Array.from(commandScopes)); +} diff --git a/src/apiv2.spec.ts b/src/apiv2.spec.ts new file mode 100644 index 00000000000..705df39cd21 --- /dev/null +++ b/src/apiv2.spec.ts @@ -0,0 +1,562 @@ +import { createServer, Server } from "http"; +import { expect } from "chai"; +import * as nock from "nock"; +import AbortController from "abort-controller"; +const proxySetup = require("proxy"); + +import { Client } from "./apiv2"; +import { FirebaseError } from "./error"; +import { streamToString, stringToStream } from "./utils"; + +describe("apiv2", () => { + beforeEach(() => { + // The api module has package variables that we don't want sticking around. + delete require.cache[require.resolve("./apiv2")]; + + nock.cleanAll(); + }); + + after(() => { + delete require.cache[require.resolve("./apiv2")]; + }); + + describe("request", () => { + it("should throw on a basic 404 GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(404, { message: "not found" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = c.request({ + method: "GET", + path: "/path/to/foo", + }); + await expect(r).to.eventually.be.rejectedWith(FirebaseError, /Not Found/); + expect(nock.isDone()).to.be.true; + }); + + it("should be able to resolve on a 404 GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(404, { message: "not found" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + resolveOnHTTPError: true, + }); + expect(r.status).to.equal(404); + expect(r.body).to.deep.equal({ message: "not found" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + }); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should be able to handle specified retry codes", async () => { + nock("https://example.com").get("/path/to/foo").once().reply(503, {}); + nock("https://example.com").get("/path/to/foo").once().reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + retryCodes: [503], + retries: 1, + retryMinTimeout: 10, + retryMaxTimeout: 15, + }); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should return an error if the retry never succeeds", async () => { + nock("https://example.com").get("/path/to/foo").twice().reply(503, {}); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = c.request({ + method: "GET", + path: "/path/to/foo", + retryCodes: [503], + retries: 1, + retryMinTimeout: 10, + retryMaxTimeout: 15, + }); + await expect(r).to.eventually.be.rejectedWith(FirebaseError, /503.+Error/); + expect(nock.isDone()).to.be.true; + }); + + it("should be able to resolve the error response if retry codes never succeed", async () => { + nock("https://example.com").get("/path/to/foo").twice().reply(503, {}); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + resolveOnHTTPError: true, + retryCodes: [503], + retries: 1, + retryMinTimeout: 10, + retryMaxTimeout: 15, + }); + expect(r.status).to.equal(503); + expect(nock.isDone()).to.be.true; + }); + + it("should not allow resolving on http error when streaming", async () => { + const c = new Client({ urlPrefix: "https://example.com" }); + const r = c.request({ + method: "GET", + path: "/path/to/foo", + responseType: "stream", + resolveOnHTTPError: false, + }); + await expect(r).to.eventually.be.rejectedWith(FirebaseError, /streaming.+resolveOnHTTPError/); + }); + + it("should be able to stream a GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(200, "ablobofdata"); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + responseType: "stream", + resolveOnHTTPError: true, + }); + const data = await streamToString(r.body); + expect(data).to.deep.equal("ablobofdata"); + expect(nock.isDone()).to.be.true; + }); + + it("should set a bearer token to 'owner' if making an insecure, local request", async () => { + nock("http://localhost") + .get("/path/to/foo") + .matchHeader("Authorization", "Bearer owner") + .reply(200, { request: "insecure" }); + + const c = new Client({ urlPrefix: "http://localhost" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + }); + expect(r.body).to.deep.equal({ request: "insecure" }); + expect(nock.isDone()).to.be.true; + }); + + it("should error with a FirebaseError if JSON is malformed", async () => { + nock("https://example.com").get("/path/to/foo").reply(200, `{not:"json"}`); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = c.request({ + method: "GET", + path: "/path/to/foo", + }); + await expect(r).to.eventually.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); + + it("should error with a FirebaseError if an error happens", async () => { + nock("https://example.com").get("/path/to/foo").replyWithError("boom"); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = c.request({ + method: "GET", + path: "/path/to/foo", + }); + await expect(r).to.eventually.be.rejectedWith(FirebaseError, /Failed to make request.+/); + expect(nock.isDone()).to.be.true; + }); + + it("should error with a FirebaseError if an invalid responseType is provided", async () => { + nock("https://example.com").get("/path/to/foo").reply(200, ""); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = c.request({ + method: "GET", + path: "/path/to/foo", + // Don't really do this. This is for testing only. + responseType: "notjson" as "json", + }); + await expect(r).to.eventually.be.rejectedWith( + FirebaseError, + /Unable to interpret response.+/, + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve a 400 GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(400, "who dis?"); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + responseType: "stream", + resolveOnHTTPError: true, + }); + expect(r.status).to.equal(400); + expect(await streamToString(r.body)).to.equal("who dis?"); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve a 404 GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(404, "not here"); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + responseType: "stream", + resolveOnHTTPError: true, + }); + expect(r.status).to.equal(404); + expect(await streamToString(r.body)).to.equal("not here"); + expect(nock.isDone()).to.be.true; + }); + + it("should be able to resolve a stream on a 404 GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(404, "does not exist"); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + responseType: "stream", + resolveOnHTTPError: true, + }); + const data = await streamToString(r.body); + expect(data).to.deep.equal("does not exist"); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic GET request if path didn't include a leading slash", async () => { + nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "path/to/foo", + }); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic GET request if urlPrefix did have a trailing slash", async () => { + nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com/" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + }); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic GET request with an api version", async () => { + nock("https://example.com").get("/v1/path/to/foo").reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com", apiVersion: "v1" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + }); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic GET request with a query string", async () => { + nock("https://example.com") + .get("/path/to/foo") + .query({ key: "value" }) + .reply(200, { success: true }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + queryParams: { key: "value" }, + }); + expect(r.body).to.deep.equal({ success: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic GET request and not override the user-agent", async () => { + nock("https://example.com") + .get("/path/to/foo") + .matchHeader("user-agent", "unit tests, silly") + .reply(200, { success: true }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + headers: { "user-agent": "unit tests, silly" }, + }); + expect(r.body).to.deep.equal({ success: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic GET request and set x-goog-user-project if GOOGLE_CLOUD_QUOTA_PROJECT is set", async () => { + nock("https://example.com") + .get("/path/to/foo") + .matchHeader("x-goog-user-project", "unit tests, silly") + .reply(200, { success: true }); + const prev = process.env["GOOGLE_CLOUD_QUOTA_PROJECT"]; + process.env["GOOGLE_CLOUD_QUOTA_PROJECT"] = "unit tests, silly"; + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + headers: { "x-goog-user-project": "unit tests, silly" }, + }); + process.env["GOOGLE_CLOUD_QUOTA_PROJECT"] = prev; + + expect(r.body).to.deep.equal({ success: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should allow explicitly ignoring GOOGLE_CLOUD_QUOTA_PROJECT", async () => { + nock("https://example.com") + .get("/path/to/foo") + .reply(function (this: nock.ReplyFnContext): nock.ReplyFnResult { + expect(this.req.headers["x-goog-user-project"]).is.undefined; + return [200, { success: true }]; + }); + const prev = process.env["GOOGLE_CLOUD_QUOTA_PROJECT"]; + process.env["GOOGLE_CLOUD_QUOTA_PROJECT"] = "unit tests, silly"; + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + ignoreQuotaProject: true, + }); + process.env["GOOGLE_CLOUD_QUOTA_PROJECT"] = prev; + + expect(r.body).to.deep.equal({ success: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should handle a 204 response with no data", async () => { + nock("https://example.com").get("/path/to/foo").reply(204); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + }); + expect(r.body).to.deep.equal(undefined); + expect(nock.isDone()).to.be.true; + }); + + it("should be able to time out if the request takes too long", async () => { + nock("https://example.com").get("/path/to/foo").delay(200).reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com/" }); + await expect( + c.request({ + method: "GET", + path: "/path/to/foo", + timeout: 10, + }), + ).to.eventually.be.rejectedWith(FirebaseError, "Timeout reached making request"); + expect(nock.isDone()).to.be.true; + }); + + it("should be able to be killed by a signal", async () => { + nock("https://example.com").get("/path/to/foo").delay(200).reply(200, { foo: "bar" }); + + const controller = new AbortController(); + setTimeout(() => controller.abort(), 10); + const c = new Client({ urlPrefix: "https://example.com/" }); + await expect( + c.request({ + method: "GET", + path: "/path/to/foo", + signal: controller.signal, + }), + ).to.eventually.be.rejectedWith(FirebaseError, "Timeout reached making request"); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic POST request", async () => { + const POST_DATA = { post: "data" }; + nock("https://example.com") + .matchHeader("Content-Type", "application/json") + .post("/path/to/foo", POST_DATA) + .reply(200, { success: true }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "POST", + path: "/path/to/foo", + body: POST_DATA, + }); + expect(r.body).to.deep.equal({ success: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic POST request without overriding Content-Type", async () => { + const POST_DATA = { post: "data" }; + nock("https://example.com") + .matchHeader("Content-Type", "application/json+customcontent") + .post("/path/to/foo", POST_DATA) + .reply(200, { success: true }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "POST", + path: "/path/to/foo", + body: POST_DATA, + headers: { "Content-Type": "application/json+customcontent" }, + }); + expect(r.body).to.deep.equal({ success: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a basic POST request with a stream", async () => { + nock("https://example.com").post("/path/to/foo", "hello world").reply(200, { success: true }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "POST", + path: "/path/to/foo", + body: stringToStream("hello world"), + }); + expect(r.body).to.deep.equal({ success: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should preserve XML messages", async () => { + const xml = "Hello!"; + nock("https://example.com").get("/path/to/foo").reply(200, xml); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + responseType: "xml", + }); + expect(r.body).to.deep.equal(xml); + expect(nock.isDone()).to.be.true; + }); + + it("should preserve XML messages on error", async () => { + const xml = + "EntityTooLarge"; + nock("https://example.com").get("/path/to/foo").reply(400, xml); + + const c = new Client({ urlPrefix: "https://example.com" }); + await expect( + c.request({ + method: "GET", + path: "/path/to/foo", + responseType: "xml", + }), + ).to.eventually.be.rejectedWith(FirebaseError, /EntityTooLarge/); + expect(nock.isDone()).to.be.true; + }); + + describe("with a proxy", () => { + let proxyServer: Server; + let targetServer: Server; + let oldProxy: string | undefined; + before(async () => { + proxyServer = proxySetup(createServer()); + targetServer = createServer((req, res) => { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ proxied: true })); + }); + await Promise.all([ + new Promise((resolve) => { + proxyServer.listen(52672, () => resolve()); + }), + new Promise((resolve) => { + targetServer.listen(52673, () => resolve()); + }), + ]); + oldProxy = process.env.HTTP_PROXY; + process.env.HTTP_PROXY = "http://127.0.0.1:52672"; + }); + + after(async () => { + await Promise.all([ + new Promise((resolve) => proxyServer.close(resolve)), + new Promise((resolve) => targetServer.close(resolve)), + ]); + process.env.HTTP_PROXY = oldProxy; + }); + + it("should be able to make a basic GET request", async () => { + const c = new Client({ + urlPrefix: "http://127.0.0.1:52673", + }); + const r = await c.request({ + method: "GET", + path: "/path/to/foo", + }); + expect(r.body).to.deep.equal({ proxied: true }); + expect(nock.isDone()).to.be.true; + }); + }); + }); + + describe("verbs", () => { + it("should make a GET request", async () => { + nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.get("/path/to/foo"); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a POST request", async () => { + const POST_DATA = { post: "data" }; + nock("https://example.com").post("/path/to/foo", POST_DATA).reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.post("/path/to/foo", POST_DATA); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a PUT request", async () => { + const DATA = { post: "data" }; + nock("https://example.com").put("/path/to/foo", DATA).reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.put("/path/to/foo", DATA); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a PATCH request", async () => { + const DATA = { post: "data" }; + nock("https://example.com").patch("/path/to/foo", DATA).reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.patch("/path/to/foo", DATA); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a DELETE request", async () => { + nock("https://example.com").delete("/path/to/foo").reply(200, { foo: "bar" }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.delete("/path/to/foo"); + expect(r.body).to.deep.equal({ foo: "bar" }); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/apiv2.ts b/src/apiv2.ts index fd51c779e83..de6e7c11d5f 100644 --- a/src/apiv2.ts +++ b/src/apiv2.ts @@ -1,7 +1,8 @@ import { AbortSignal } from "abort-controller"; +import { URL, URLSearchParams } from "url"; import { Readable } from "stream"; -import { parse, URLSearchParams } from "url"; -import * as ProxyAgent from "proxy-agent"; +import { ProxyAgent } from "proxy-agent"; +import * as retry from "retry"; import AbortController from "abort-controller"; import fetch, { HeadersInit, Response, RequestInit, Headers } from "node-fetch"; import util from "util"; @@ -9,22 +10,44 @@ import util from "util"; import * as auth from "./auth"; import { FirebaseError } from "./error"; import { logger } from "./logger"; -import * as responseToError from "./responseToError"; +import { responseToError } from "./responseToError"; +import * as FormData from "form-data"; // Using import would require resolveJsonModule, which seems to break the // build/output format. const pkg = require("../package.json"); const CLI_VERSION: string = pkg.version; -export type HttpMethod = "GET" | "PUT" | "POST" | "DELETE" | "PATCH"; +export const STANDARD_HEADERS: Record = { + Connection: "keep-alive", + "User-Agent": `FirebaseCLI/${CLI_VERSION}`, + "X-Client-Version": `FirebaseCLI/${CLI_VERSION}`, +}; + +const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user"; + +const GOOG_USER_PROJECT_HEADER = "x-goog-user-project"; +const GOOGLE_CLOUD_QUOTA_PROJECT = process.env.GOOGLE_CLOUD_QUOTA_PROJECT; + +export type HttpMethod = + | "GET" + | "PUT" + | "POST" + | "DELETE" + | "PATCH" + | "OPTIONS" + | "HEAD" + | "CONNECT" + | "TRACE"; interface BaseRequestOptions extends VerbOptions { method: HttpMethod; path: string; body?: T | string | NodeJS.ReadableStream; - responseType?: "json" | "stream"; + responseType?: "json" | "xml" | "stream" | "arraybuffer" | "blob" | "text" | "unknown"; redirect?: "error" | "follow" | "manual"; compress?: boolean; + ignoreQuotaProject?: boolean; } interface RequestOptionsWithSignal extends BaseRequestOptions { @@ -54,6 +77,14 @@ interface ClientHandlingOptions { resBody?: boolean; }; resolveOnHTTPError?: boolean; + /** Codes on which to retry. Defaults to none. */ + retryCodes?: number[]; + /** Number of retries. Defaults to 0 (one attempt) with no retryCodes, 1 with retryCodes. */ + retries?: number; + /** Minimum timeout between retries. Defaults to 1s. */ + retryMinTimeout?: number; + /** Maximum timeout between retries. Defaults to 5s. */ + retryMaxTimeout?: number; } export type ClientRequestOptions = RequestOptions & ClientVerbOptions; @@ -91,6 +122,18 @@ export function setAccessToken(token = ""): void { accessToken = token; } +/** + * Gets a singleton access token + * @returns An access token + */ +export async function getAccessToken(): Promise { + if (accessToken) { + return accessToken; + } + const data = await auth.getAccessToken(refreshToken, []); + return data.access_token; +} + function proxyURIFromEnv(): string | undefined { return ( process.env.HTTPS_PROXY || @@ -105,7 +148,6 @@ export type ClientOptions = { urlPrefix: string; apiVersion?: string; auth?: boolean; - proxy?: string; }; export class Client { @@ -129,7 +171,7 @@ export class Client { post( path: string, json?: ReqT, - options: ClientVerbOptions = {} + options: ClientVerbOptions = {}, ): Promise> { const reqOptions: ClientRequestOptions = Object.assign(options, { method: "POST", @@ -142,7 +184,7 @@ export class Client { patch( path: string, json?: ReqT, - options: ClientVerbOptions = {} + options: ClientVerbOptions = {}, ): Promise> { const reqOptions: ClientRequestOptions = Object.assign(options, { method: "PATCH", @@ -155,7 +197,7 @@ export class Client { put( path: string, json?: ReqT, - options: ClientVerbOptions = {} + options: ClientVerbOptions = {}, ): Promise> { const reqOptions: ClientRequestOptions = Object.assign(options, { method: "PUT", @@ -173,6 +215,13 @@ export class Client { return this.request(reqOptions); } + options(path: string, options: ClientVerbOptions = {}): Promise> { + const reqOptions: ClientRequestOptions = Object.assign(options, { + method: "OPTIONS", + path, + }); + return this.request(reqOptions); + } /** * Makes a request as specified by the options. * By default, this will: @@ -201,7 +250,7 @@ export class Client { if (reqOptions.responseType === "stream" && !reqOptions.resolveOnHTTPError) { throw new FirebaseError( "apiv2 will not handle HTTP errors while streaming and you must set `resolveOnHTTPError` and check for res.status >= 400 on your own", - { exit: 2 } + { exit: 2 }, ); } @@ -216,7 +265,7 @@ export class Client { } try { return await this.doRequest(internalReqOptions); - } catch (thrown) { + } catch (thrown: any) { if (thrown instanceof FirebaseError) { throw thrown; } @@ -232,26 +281,33 @@ export class Client { } private addRequestHeaders( - reqOptions: InternalClientRequestOptions + reqOptions: InternalClientRequestOptions, ): InternalClientRequestOptions { if (!reqOptions.headers) { reqOptions.headers = new Headers(); } - reqOptions.headers.set("Connection", "keep-alive"); - if (!reqOptions.headers.has("User-Agent")) { - reqOptions.headers.set("User-Agent", `FirebaseCLI/${CLI_VERSION}`); + for (const [h, v] of Object.entries(STANDARD_HEADERS)) { + if (!reqOptions.headers.has(h)) { + reqOptions.headers.set(h, v); + } } - reqOptions.headers.set("X-Client-Version", `FirebaseCLI/${CLI_VERSION}`); if (!reqOptions.headers.has("Content-Type")) { if (reqOptions.responseType === "json") { reqOptions.headers.set("Content-Type", "application/json"); } } + if ( + !reqOptions.ignoreQuotaProject && + GOOGLE_CLOUD_QUOTA_PROJECT && + GOOGLE_CLOUD_QUOTA_PROJECT !== "" + ) { + reqOptions.headers.set(GOOG_USER_PROJECT_HEADER, GOOGLE_CLOUD_QUOTA_PROJECT); + } return reqOptions; } private async addAuthHeader( - reqOptions: InternalClientRequestOptions + reqOptions: InternalClientRequestOptions, ): Promise> { if (!reqOptions.headers) { reqOptions.headers = new Headers(); @@ -260,33 +316,19 @@ export class Client { if (isLocalInsecureRequest(this.opts.urlPrefix)) { token = "owner"; } else { - token = await this.getAccessToken(); + token = await getAccessToken(); } reqOptions.headers.set("Authorization", `Bearer ${token}`); return reqOptions; } - private async getAccessToken(): Promise { - // Runtime fetch of Auth singleton to prevent circular module dependencies - if (accessToken) { - return accessToken; - } - // TODO: remove the as any once auth.js is migrated to auth.ts - interface AccessToken { - /* eslint-disable camelcase */ - access_token: string; - } - const data = (await auth.getAccessToken(refreshToken, [])) as AccessToken; - return data.access_token; - } - private requestURL(options: InternalClientRequestOptions): string { const versionPath = this.opts.apiVersion ? `/${this.opts.apiVersion}` : ""; return `${this.opts.urlPrefix}${versionPath}${options.path}`; } private async doRequest( - options: InternalClientRequestOptions + options: InternalClientRequestOptions, ): Promise> { if (!options.path.startsWith("/")) { options.path = "/" + options.path; @@ -315,12 +357,8 @@ export class Client { compress: options.compress, }; - if (this.opts.proxy) { - fetchOptions.agent = new ProxyAgent(this.opts.proxy); - } - const envProxy = proxyURIFromEnv(); - if (envProxy) { - fetchOptions.agent = new ProxyAgent(envProxy); + if (proxyURIFromEnv()) { + fetchOptions.agent = new ProxyAgent(); } if (options.signal) { @@ -342,57 +380,106 @@ export class Client { fetchOptions.body = JSON.stringify(options.body); } - this.logRequest(options); - - let res: Response; - try { - res = await fetch(fetchURL, fetchOptions); - } catch (thrown) { - const err = thrown instanceof Error ? thrown : new Error(thrown); - const isAbortError = err.name.includes("AbortError"); - if (isAbortError) { - throw new FirebaseError(`Timeout reached making request to ${fetchURL}`, { original: err }); - } - throw new FirebaseError(`Failed to make request to ${fetchURL}`, { original: err }); - } finally { - // If we succeed or failed, clear the timeout. - if (reqTimeout) { - clearTimeout(reqTimeout); - } + // TODO(bkendall): Refactor this to use Throttler _or_ refactor Throttle to use `retry`. + const operationOptions: retry.OperationOptions = { + retries: options.retryCodes?.length ? 1 : 2, + minTimeout: 1 * 1000, + maxTimeout: 5 * 1000, + }; + if (typeof options.retries === "number") { + operationOptions.retries = options.retries; } - - let body: ResT; - if (options.responseType === "json") { - const text = await res.text(); - // Some responses, such as 204 and occasionally 202s don't have - // any content. We can't just rely on response code (202 may have conent) - // and unfortuantely res.length is unreliable (many requests return zero). - if (!text.length) { - body = (undefined as unknown) as ResT; - } else { - body = JSON.parse(text) as ResT; - } - } else if (options.responseType === "stream") { - body = (res.body as unknown) as ResT; - } else { - throw new FirebaseError(`Unable to interpret response. Please set responseType.`, { - exit: 2, - }); + if (typeof options.retryMinTimeout === "number") { + operationOptions.minTimeout = options.retryMinTimeout; } - - this.logResponse(res, body, options); - - if (res.status >= 400) { - if (!options.resolveOnHTTPError) { - throw responseToError({ statusCode: res.status }, body); - } + if (typeof options.retryMaxTimeout === "number") { + operationOptions.maxTimeout = options.retryMaxTimeout; } + const operation = retry.operation(operationOptions); + + return await new Promise>((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + operation.attempt(async (currentAttempt): Promise => { + let res: Response; + let body: ResT; + try { + if (currentAttempt > 1) { + logger.debug( + `*** [apiv2] Attempting the request again. Attempt number ${currentAttempt}`, + ); + } + this.logRequest(options); + try { + res = await fetch(fetchURL, fetchOptions); + } catch (thrown: any) { + const err = thrown instanceof Error ? thrown : new Error(thrown); + logger.debug( + `*** [apiv2] error from fetch(${fetchURL}, ${JSON.stringify(fetchOptions)}): ${err}`, + ); + const isAbortError = err.name.includes("AbortError"); + if (isAbortError) { + throw new FirebaseError(`Timeout reached making request to ${fetchURL}`, { + original: err, + }); + } + throw new FirebaseError(`Failed to make request to ${fetchURL}`, { original: err }); + } finally { + // If we succeed or failed, clear the timeout. + if (reqTimeout) { + clearTimeout(reqTimeout); + } + } + + if (options.responseType === "json") { + const text = await res.text(); + // Some responses, such as 204 and occasionally 202s don't have + // any content. We can't just rely on response code (202 may have conent) + // and unfortuantely res.length is unreliable (many requests return zero). + if (!text.length) { + body = undefined as unknown as ResT; + } else { + try { + body = JSON.parse(text) as ResT; + } catch (err: unknown) { + // JSON-parse errors are useless. Log the response for better debugging. + this.logResponse(res, text, options); + throw new FirebaseError(`Unable to parse JSON: ${err}`); + } + } + } else if (options.responseType === "xml") { + body = (await res.text()) as unknown as ResT; + } else if (options.responseType === "stream") { + body = res.body as unknown as ResT; + } else { + throw new FirebaseError(`Unable to interpret response. Please set responseType.`, { + exit: 2, + }); + } + } catch (err: unknown) { + return err instanceof FirebaseError ? reject(err) : reject(new FirebaseError(`${err}`)); + } - return { - status: res.status, - response: res, - body, - }; + this.logResponse(res, body, options); + + if (res.status >= 400) { + if (options.retryCodes?.includes(res.status)) { + const err = responseToError({ statusCode: res.status }, body) || undefined; + if (operation.retry(err)) { + return; + } + } + if (!options.resolveOnHTTPError) { + return reject(responseToError({ statusCode: res.status }, body)); + } + } + + resolve({ + status: res.status, + response: res, + body, + }); + }); + }); } private logRequest(options: InternalClientRequestOptions): void { @@ -408,6 +495,14 @@ export class Client { } const logURL = this.requestURL(options); logger.debug(`>>> [apiv2][query] ${options.method} ${logURL} ${queryParamsLog}`); + const headers = options.headers; + if (headers && headers.has(GOOG_QUOTA_USER_HEADER)) { + logger.debug( + `>>> [apiv2][(partial)header] ${options.method} ${logURL} x-goog-quota-user=${ + headers.get(GOOG_QUOTA_USER_HEADER) || "" + }`, + ); + } if (options.body !== undefined) { let logBody = "[omitted]"; if (!options.skipLog?.body) { @@ -420,7 +515,7 @@ export class Client { private logResponse( res: Response, body: unknown, - options: InternalClientRequestOptions + options: InternalClientRequestOptions, ): void { const logURL = this.requestURL(options); logger.debug(`<<< [apiv2][status] ${options.method} ${logURL} ${res.status}`); @@ -433,7 +528,7 @@ export class Client { } function isLocalInsecureRequest(urlPrefix: string): boolean { - const u = parse(urlPrefix); + const u = new URL(urlPrefix); return u.protocol === "http:"; } @@ -451,5 +546,5 @@ function bodyToString(body: unknown): string { } function isStream(o: unknown): o is NodeJS.ReadableStream { - return o instanceof Readable; + return o instanceof Readable || o instanceof FormData; } diff --git a/src/appdistribution/client.spec.ts b/src/appdistribution/client.spec.ts new file mode 100644 index 00000000000..85d1251b30d --- /dev/null +++ b/src/appdistribution/client.spec.ts @@ -0,0 +1,382 @@ +import { expect } from "chai"; +import { join } from "path"; +import * as fs from "fs-extra"; +import * as nock from "nock"; +import * as rimraf from "rimraf"; +import * as sinon from "sinon"; +import * as tmp from "tmp"; + +import { AppDistributionClient } from "./client"; +import { BatchRemoveTestersResponse, Group, TestDevice } from "./types"; +import { appDistributionOrigin } from "../api"; +import { Distribution } from "./distribution"; +import { FirebaseError } from "../error"; + +tmp.setGracefulCleanup(); + +describe("distribution", () => { + const tempdir = tmp.dirSync(); + const projectName = "projects/123456789"; + const appName = `${projectName}/apps/1:123456789:ios:abc123def456`; + const groupName = `${projectName}/groups/my-group`; + const binaryFile = join(tempdir.name, "app.ipa"); + fs.ensureFileSync(binaryFile); + const mockDistribution = new Distribution(binaryFile); + const appDistributionClient = new AppDistributionClient(); + + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.useFakeTimers(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + after(() => { + rimraf.sync(tempdir.name); + }); + + describe("addTesters", () => { + const emails = ["a@foo.com", "b@foo.com"]; + + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${projectName}/testers:batchAdd`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.addTesters(projectName, emails)).to.be.rejectedWith( + FirebaseError, + "Failed to add testers", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()).post(`/v1/${projectName}/testers:batchAdd`).reply(200, {}); + await expect(appDistributionClient.addTesters(projectName, emails)).to.be.eventually + .fulfilled; + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deleteTesters", () => { + const emails = ["a@foo.com", "b@foo.com"]; + const mockResponse: BatchRemoveTestersResponse = { emails: emails }; + + it("should throw error if delete fails", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${projectName}/testers:batchRemove`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.removeTesters(projectName, emails)).to.be.rejectedWith( + FirebaseError, + "Failed to remove testers", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${projectName}/testers:batchRemove`) + .reply(200, mockResponse); + await expect(appDistributionClient.removeTesters(projectName, emails)).to.eventually.deep.eq( + mockResponse, + ); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("uploadRelease", () => { + it("should throw error if upload fails", async () => { + nock(appDistributionOrigin()).post(`/upload/v1/${appName}/releases:upload`).reply(400, {}); + await expect(appDistributionClient.uploadRelease(appName, mockDistribution)).to.be.rejected; + expect(nock.isDone()).to.be.true; + }); + + it("should return token if upload succeeds", async () => { + const fakeOperation = "fake-operation-name"; + nock(appDistributionOrigin()) + .post(`/upload/v1/${appName}/releases:upload`) + .reply(200, { name: fakeOperation }); + await expect( + appDistributionClient.uploadRelease(appName, mockDistribution), + ).to.be.eventually.eq(fakeOperation); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateReleaseNotes", () => { + const releaseName = `${appName}/releases/fake-release-id`; + it("should return immediately when no release notes are specified", async () => { + await expect(appDistributionClient.updateReleaseNotes(releaseName, "")).to.eventually.be + .fulfilled; + expect(nock.isDone()).to.be.true; + }); + + it("should throw error when request fails", async () => { + nock(appDistributionOrigin()) + .patch(`/v1/${releaseName}?updateMask=release_notes.text`) + .reply(400, {}); + await expect( + appDistributionClient.updateReleaseNotes(releaseName, "release notes"), + ).to.be.rejectedWith(FirebaseError, "failed to update release notes"); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()) + .patch(`/v1/${releaseName}?updateMask=release_notes.text`) + .reply(200, {}); + await expect(appDistributionClient.updateReleaseNotes(releaseName, "release notes")).to + .eventually.be.fulfilled; + expect(nock.isDone()).to.be.true; + }); + }); + + describe("distribute", () => { + const releaseName = `${appName}/releases/fake-release-id`; + it("should return immediately when testers and groups are empty", async () => { + await expect(appDistributionClient.distribute(releaseName)).to.eventually.be.fulfilled; + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()).post(`/v1/${releaseName}:distribute`).reply(200, {}); + await expect(appDistributionClient.distribute(releaseName, ["tester1"], ["group1"])).to.be + .fulfilled; + expect(nock.isDone()).to.be.true; + }); + + describe("when request fails", () => { + let testers: string[]; + let groups: string[]; + beforeEach(() => { + testers = ["tester1"]; + groups = ["group1"]; + }); + + it("should throw invalid testers error when status code is FAILED_PRECONDITION ", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${releaseName}:distribute`, { + testerEmails: testers, + groupAliases: groups, + }) + .reply(412, { error: { status: "FAILED_PRECONDITION" } }); + await expect( + appDistributionClient.distribute(releaseName, testers, groups), + ).to.be.rejectedWith( + FirebaseError, + "failed to distribute to testers/groups: invalid testers", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should throw invalid groups error when status code is INVALID_ARGUMENT", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${releaseName}:distribute`, { + testerEmails: testers, + groupAliases: groups, + }) + .reply(412, { error: { status: "INVALID_ARGUMENT" } }); + await expect( + appDistributionClient.distribute(releaseName, testers, groups), + ).to.be.rejectedWith( + FirebaseError, + "failed to distribute to testers/groups: invalid groups", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should throw default error", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${releaseName}:distribute`, { + testerEmails: testers, + groupAliases: groups, + }) + .reply(400, {}); + await expect( + appDistributionClient.distribute(releaseName, ["tester1"], ["group1"]), + ).to.be.rejectedWith(FirebaseError, "failed to distribute to testers/groups"); + expect(nock.isDone()).to.be.true; + }); + }); + }); + + describe("createGroup", () => { + const mockResponse: Group = { name: groupName, displayName: "My Group" }; + + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${projectName}/groups`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.createGroup(projectName, "My Group")).to.be.rejectedWith( + FirebaseError, + "Failed to create group", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()).post(`/v1/${projectName}/groups`).reply(200, mockResponse); + await expect( + appDistributionClient.createGroup(projectName, "My Group"), + ).to.eventually.deep.eq(mockResponse); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request with alias succeeds", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${projectName}/groups?groupId=my-group`) + .reply(200, mockResponse); + await expect( + appDistributionClient.createGroup(projectName, "My Group", "my-group"), + ).to.eventually.deep.eq(mockResponse); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deleteGroup", () => { + it("should throw error if delete fails", async () => { + nock(appDistributionOrigin()) + .delete(`/v1/${groupName}`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.deleteGroup(groupName)).to.be.rejectedWith( + FirebaseError, + "Failed to delete group", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()).delete(`/v1/${groupName}`).reply(200, {}); + await expect(appDistributionClient.deleteGroup(groupName)).to.be.eventually.fulfilled; + expect(nock.isDone()).to.be.true; + }); + }); + + describe("addTestersToGroup", () => { + const emails = ["a@foo.com", "b@foo.com"]; + + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${groupName}:batchJoin`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.addTestersToGroup(groupName, emails)).to.be.rejectedWith( + FirebaseError, + "Failed to add testers to group", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()).post(`/v1/${groupName}:batchJoin`).reply(200, {}); + await expect(appDistributionClient.addTestersToGroup(groupName, emails)).to.be.eventually + .fulfilled; + expect(nock.isDone()).to.be.true; + }); + }); + + describe("removeTestersFromGroup", () => { + const emails = ["a@foo.com", "b@foo.com"]; + + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .post(`/v1/${groupName}:batchLeave`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect( + appDistributionClient.removeTestersFromGroup(groupName, emails), + ).to.be.rejectedWith(FirebaseError, "Failed to remove testers from group"); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds", async () => { + nock(appDistributionOrigin()).post(`/v1/${groupName}:batchLeave`).reply(200, {}); + await expect(appDistributionClient.removeTestersFromGroup(groupName, emails)).to.be.eventually + .fulfilled; + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createReleaseTest", () => { + const releaseName = `${appName}/releases/fake-release-id`; + const mockDevices: TestDevice[] = [ + { + model: "husky", + version: "34", + orientation: "portrait", + locale: "en-US", + }, + { + model: "bluejay", + version: "32", + orientation: "landscape", + locale: "es", + }, + ]; + const mockReleaseTest = { + name: `${releaseName}/tests/fake-test-id`, + devices: mockDevices, + state: "IN_PROGRESS", + }; + + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .post(`/v1alpha/${releaseName}/tests`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect( + appDistributionClient.createReleaseTest(releaseName, mockDevices), + ).to.be.rejectedWith(FirebaseError, "Failed to create release test"); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with ReleaseTest when request succeeds", async () => { + nock(appDistributionOrigin()) + .post(`/v1alpha/${releaseName}/tests`) + .reply(200, mockReleaseTest); + await expect( + appDistributionClient.createReleaseTest(releaseName, mockDevices), + ).to.be.eventually.deep.eq(mockReleaseTest); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getReleaseTest", () => { + const releaseTestName = `${appName}/releases/fake-release-id/tests/fake-test-id`; + const mockDevices: TestDevice[] = [ + { + model: "husky", + version: "34", + orientation: "portrait", + locale: "en-US", + }, + { + model: "bluejay", + version: "32", + orientation: "landscape", + locale: "es", + }, + ]; + const mockReleaseTest = { + name: releaseTestName, + devices: mockDevices, + state: "IN_PROGRESS", + }; + + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .get(`/v1alpha/${releaseTestName}`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.getReleaseTest(releaseTestName)).to.be.rejected; + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with ReleaseTest when request succeeds", async () => { + nock(appDistributionOrigin()).get(`/v1alpha/${releaseTestName}`).reply(200, mockReleaseTest); + await expect(appDistributionClient.getReleaseTest(releaseTestName)).to.be.eventually.deep.eq( + mockReleaseTest, + ); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/appdistribution/client.ts b/src/appdistribution/client.ts index 219e79388f2..66998ce88ea 100644 --- a/src/appdistribution/client.ts +++ b/src/appdistribution/client.ts @@ -1,103 +1,60 @@ -import * as _ from "lodash"; -import * as api from "../api"; +import { ReadStream } from "fs"; + import * as utils from "../utils"; import * as operationPoller from "../operation-poller"; import { Distribution } from "./distribution"; import { FirebaseError } from "../error"; -import { Client, ClientResponse } from "../apiv2"; - -/** - * Helper interface for an app that is provisioned with App Distribution - */ -export interface AabInfo { - name: string; - integrationState: IntegrationState; - testCertificate: TestCertificate | null; -} - -export interface TestCertificate { - hashSha1: string; - hashSha256: string; - hashMd5: string; -} - -/** Enum representing the App Bundles state for the App */ -export enum IntegrationState { - AAB_INTEGRATION_STATE_UNSPECIFIED = "AAB_INTEGRATION_STATE_UNSPECIFIED", - INTEGRATED = "INTEGRATED", - PLAY_ACCOUNT_NOT_LINKED = "PLAY_ACCOUNT_NOT_LINKED", - NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT = "NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT", - APP_NOT_PUBLISHED = "APP_NOT_PUBLISHED", - AAB_STATE_UNAVAILABLE = "AAB_STATE_UNAVAILABLE", - PLAY_IAS_TERMS_NOT_ACCEPTED = "PLAY_IAS_TERMS_NOT_ACCEPTED", -} - -export enum UploadReleaseResult { - UPLOAD_RELEASE_RESULT_UNSPECIFIED = "UPLOAD_RELEASE_RESULT_UNSPECIFIED", - RELEASE_CREATED = "RELEASE_CREATED", - RELEASE_UPDATED = "RELEASE_UPDATED", - RELEASE_UNMODIFIED = "RELEASE_UNMODIFIED", -} - -export interface Release { - name: string; - releaseNotes: ReleaseNotes; - displayVersion: string; - buildVersion: string; - createTime: Date; -} - -export interface ReleaseNotes { - text: string; -} - -export interface UploadReleaseResponse { - result: UploadReleaseResult; - release: Release; -} - -export interface BatchRemoveTestersResponse { - emails: string[]; -} +import { Client } from "../apiv2"; +import { appDistributionOrigin } from "../api"; +import { + AabInfo, + BatchRemoveTestersResponse, + Group, + LoginCredential, + mapDeviceToExecution, + ReleaseTest, + TestDevice, + UploadReleaseResponse, +} from "./types"; /** * Makes RPCs to the App Distribution server backend. */ export class AppDistributionClient { - appDistroV2Client = new Client({ - urlPrefix: api.appDistributionOrigin, + appDistroV1Client = new Client({ + urlPrefix: appDistributionOrigin(), apiVersion: "v1", }); + appDistroV1AlphaClient = new Client({ + urlPrefix: appDistributionOrigin(), + apiVersion: "v1alpha", + }); async getAabInfo(appName: string): Promise { - const apiResponse = await api.request("GET", `/v1/${appName}/aabInfo`, { - origin: api.appDistributionOrigin, - auth: true, - }); - - return _.get(apiResponse, "body"); + const apiResponse = await this.appDistroV1Client.get(`/${appName}/aabInfo`); + return apiResponse.body; } async uploadRelease(appName: string, distribution: Distribution): Promise { - const apiResponse = await api.request("POST", `/upload/v1/${appName}/releases:upload`, { - auth: true, - origin: api.appDistributionOrigin, + const client = new Client({ urlPrefix: appDistributionOrigin() }); + const apiResponse = await client.request({ + method: "POST", + path: `/upload/v1/${appName}/releases:upload`, headers: { - "X-Goog-Upload-File-Name": distribution.getFileName(), + "X-Goog-Upload-File-Name": encodeURIComponent(distribution.getFileName()), "X-Goog-Upload-Protocol": "raw", "Content-Type": "application/octet-stream", }, - data: distribution.readStream(), - json: false, + responseType: "json", + body: distribution.readStream(), }); - - return _.get(JSON.parse(apiResponse.body), "name"); + return apiResponse.body.name; } async pollUploadStatus(operationName: string): Promise { return operationPoller.pollOperation({ pollerName: "App Distribution Upload Poller", - apiOrigin: api.appDistributionOrigin, + apiOrigin: appDistributionOrigin(), apiVersion: "v1", operationResourceName: operationName, masterTimeout: 5 * 60 * 1000, @@ -120,15 +77,12 @@ export class AppDistributionClient { text: releaseNotes, }, }; + const queryParams = { updateMask: "release_notes.text" }; try { - await api.request("PATCH", `/v1/${releaseName}?updateMask=release_notes.text`, { - origin: api.appDistributionOrigin, - auth: true, - data, - }); - } catch (err) { - throw new FirebaseError(`failed to update release notes with ${err.message}`, { exit: 1 }); + await this.appDistroV1Client.patch(`/${releaseName}`, data, { queryParams }); + } catch (err: any) { + throw new FirebaseError(`failed to update release notes with ${err?.message}`); } utils.logSuccess("added release notes successfully"); @@ -137,7 +91,7 @@ export class AppDistributionClient { async distribute( releaseName: string, testerEmails: string[] = [], - groupAliases: string[] = [] + groupAliases: string[] = [], ): Promise { if (testerEmails.length === 0 && groupAliases.length === 0) { utils.logWarning("no testers or groups specified, skipping"); @@ -152,20 +106,14 @@ export class AppDistributionClient { }; try { - await api.request("POST", `/v1/${releaseName}:distribute`, { - origin: api.appDistributionOrigin, - auth: true, - data, - }); - } catch (err) { + await this.appDistroV1Client.post(`/${releaseName}:distribute`, data); + } catch (err: any) { let errorMessage = err.message; - if (_.has(err, "context.body.error")) { - const errorStatus = _.get(err, "context.body.error.status"); - if (errorStatus === "FAILED_PRECONDITION") { - errorMessage = "invalid testers"; - } else if (errorStatus === "INVALID_ARGUMENT") { - errorMessage = "invalid groups"; - } + const errorStatus = err?.context?.body?.error?.status; + if (errorStatus === "FAILED_PRECONDITION") { + errorMessage = "invalid testers"; + } else if (errorStatus === "INVALID_ARGUMENT") { + errorMessage = "invalid groups"; } throw new FirebaseError(`failed to distribute to testers/groups: ${errorMessage}`, { exit: 1, @@ -175,14 +123,14 @@ export class AppDistributionClient { utils.logSuccess("distributed to testers/groups successfully"); } - async addTesters(projectName: string, emails: string[]) { + async addTesters(projectName: string, emails: string[]): Promise { try { - await this.appDistroV2Client.request({ + await this.appDistroV1Client.request({ method: "POST", path: `${projectName}/testers:batchAdd`, body: { emails: emails }, }); - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to add testers ${err}`); } @@ -192,7 +140,7 @@ export class AppDistributionClient { async removeTesters(projectName: string, emails: string[]): Promise { let apiResponse; try { - apiResponse = await this.appDistroV2Client.request< + apiResponse = await this.appDistroV1Client.request< { emails: string[] }, BatchRemoveTestersResponse >({ @@ -200,9 +148,90 @@ export class AppDistributionClient { path: `${projectName}/testers:batchRemove`, body: { emails: emails }, }); - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to remove testers ${err}`); } return apiResponse.body; } + + async createGroup(projectName: string, displayName: string, alias?: string): Promise { + let apiResponse; + try { + apiResponse = await this.appDistroV1Client.request<{ displayName: string }, Group>({ + method: "POST", + path: + alias === undefined ? `${projectName}/groups` : `${projectName}/groups?groupId=${alias}`, + body: { displayName: displayName }, + }); + } catch (err: any) { + throw new FirebaseError(`Failed to create group ${err}`); + } + return apiResponse.body; + } + + async deleteGroup(groupName: string): Promise { + try { + await this.appDistroV1Client.request({ + method: "DELETE", + path: groupName, + }); + } catch (err: any) { + throw new FirebaseError(`Failed to delete group ${err}`); + } + + utils.logSuccess(`Group deleted successfully`); + } + + async addTestersToGroup(groupName: string, emails: string[]): Promise { + try { + await this.appDistroV1Client.request({ + method: "POST", + path: `${groupName}:batchJoin`, + body: { emails: emails }, + }); + } catch (err: any) { + throw new FirebaseError(`Failed to add testers to group ${err}`); + } + + utils.logSuccess(`Testers added to group successfully`); + } + + async removeTestersFromGroup(groupName: string, emails: string[]): Promise { + try { + await this.appDistroV1Client.request({ + method: "POST", + path: `${groupName}:batchLeave`, + body: { emails: emails }, + }); + } catch (err: any) { + throw new FirebaseError(`Failed to remove testers from group ${err}`); + } + + utils.logSuccess(`Testers removed from group successfully`); + } + + async createReleaseTest( + releaseName: string, + devices: TestDevice[], + loginCredential?: LoginCredential, + ): Promise { + try { + const response = await this.appDistroV1AlphaClient.request({ + method: "POST", + path: `${releaseName}/tests`, + body: { + deviceExecutions: devices.map(mapDeviceToExecution), + loginCredential, + }, + }); + return response.body; + } catch (err: any) { + throw new FirebaseError(`Failed to create release test ${err}`); + } + } + + async getReleaseTest(releaseTestName: string): Promise { + const response = await this.appDistroV1AlphaClient.get(releaseTestName); + return response.body; + } } diff --git a/src/appdistribution/distribution.ts b/src/appdistribution/distribution.ts index d1d2ee2dae7..3175bde6c03 100644 --- a/src/appdistribution/distribution.ts +++ b/src/appdistribution/distribution.ts @@ -33,7 +33,7 @@ export class Distribution { let stat; try { stat = fs.statSync(path); - } catch (err) { + } catch (err: any) { logger.info(err); throw new FirebaseError(`File ${path} does not exist: verify that file points to a binary`); } diff --git a/src/appdistribution/options-parser-util.spec.ts b/src/appdistribution/options-parser-util.spec.ts new file mode 100644 index 00000000000..66509ffda12 --- /dev/null +++ b/src/appdistribution/options-parser-util.spec.ts @@ -0,0 +1,205 @@ +import { expect } from "chai"; +import { getLoginCredential, getTestDevices } from "./options-parser-util"; +import { FirebaseError } from "../error"; +import * as fs from "fs-extra"; +import * as rimraf from "rimraf"; +import * as tmp from "tmp"; +import { join } from "path"; + +tmp.setGracefulCleanup(); + +describe("options-parser-util", () => { + const tempdir = tmp.dirSync(); + const passwordFile = join(tempdir.name, "password.txt"); + fs.outputFileSync(passwordFile, "password-from-file\n"); + + after(() => { + rimraf.sync(tempdir.name); + }); + + describe("getTestDevices", () => { + it("parses a test device", () => { + const optionValue = "model=modelname,version=123,orientation=landscape,locale=en_US"; + + const result = getTestDevices(optionValue, ""); + + expect(result).to.deep.equal([ + { + model: "modelname", + version: "123", + orientation: "landscape", + locale: "en_US", + }, + ]); + }); + + it("parses multiple semicolon-separated test devices", () => { + const optionValue = + "model=modelname,version=123,orientation=landscape,locale=en_US;model=modelname2,version=456,orientation=portrait,locale=es"; + + const result = getTestDevices(optionValue, ""); + + expect(result).to.deep.equal([ + { + model: "modelname", + version: "123", + orientation: "landscape", + locale: "en_US", + }, + { + model: "modelname2", + version: "456", + orientation: "portrait", + locale: "es", + }, + ]); + }); + + it("parses multiple newline-separated test devices", () => { + const optionValue = + "model=modelname,version=123,orientation=landscape,locale=en_US\nmodel=modelname2,version=456,orientation=portrait,locale=es"; + + const result = getTestDevices(optionValue, ""); + + expect(result).to.deep.equal([ + { + model: "modelname", + version: "123", + orientation: "landscape", + locale: "en_US", + }, + { + model: "modelname2", + version: "456", + orientation: "portrait", + locale: "es", + }, + ]); + }); + + it("throws an error with correct format when missing a field", () => { + const optionValue = "model=modelname,version=123,locale=en_US"; + + expect(() => getTestDevices(optionValue, "")).to.throw( + FirebaseError, + "model=,version=,locale=,orientation=", + ); + }); + + it("throws an error with expected fields when field is unexpected", () => { + const optionValue = + "model=modelname,version=123,orientation=landscape,locale=en_US,notafield=blah"; + + expect(() => getTestDevices(optionValue, "")).to.throw( + FirebaseError, + "model, version, orientation, locale", + ); + }); + }); + + describe("getLoginCredential", () => { + it("returns credential for username and password", () => { + const result = getLoginCredential({ username: "user", password: "123" }); + + expect(result).to.deep.equal({ + username: "user", + password: "123", + fieldHints: undefined, + }); + }); + + it("returns credential for username and passwordFile", () => { + const result = getLoginCredential({ username: "user", passwordFile }); + + expect(result).to.deep.equal({ + username: "user", + password: "password-from-file", + fieldHints: undefined, + }); + }); + + it("returns undefined when no options provided", () => { + const result = getLoginCredential({}); + + expect(result).to.be.undefined; + }); + + it("returns credential for username, password, and resource names", () => { + const result = getLoginCredential({ + username: "user", + password: "123", + usernameResourceName: "username_resource_id", + passwordResourceName: "password_resource_id", + }); + + expect(result).to.deep.equal({ + username: "user", + password: "123", + fieldHints: { + usernameResourceName: "username_resource_id", + passwordResourceName: "password_resource_id", + }, + }); + }); + + it("returns credential for username, passwordFile, and resource names", () => { + const result = getLoginCredential({ + username: "user", + passwordFile, + usernameResourceName: "username_resource_id", + passwordResourceName: "password_resource_id", + }); + + expect(result).to.deep.equal({ + username: "user", + password: "password-from-file", + fieldHints: { + usernameResourceName: "username_resource_id", + passwordResourceName: "password_resource_id", + }, + }); + }); + + it("throws error when username and password not provided together", () => { + expect(() => getLoginCredential({ username: "user" })).to.throw( + FirebaseError, + "Username and password for automated tests need to be specified together", + ); + }); + + it("throws error when password (but not username) resource provided", () => { + expect(() => + getLoginCredential({ + username: "user", + password: "123", + passwordResourceName: "password_resource_id", + }), + ).to.throw( + FirebaseError, + "Username and password resource names for automated tests need to be specified together", + ); + }); + + it("throws error when password file and password (but not username) resource provided", () => { + expect(() => + getLoginCredential({ + username: "user", + passwordFile, + passwordResourceName: "password_resource_id", + }), + ).to.throw( + FirebaseError, + "Username and password resource names for automated tests need to be specified together", + ); + }); + + it("throws error when resource names provided without username and password", () => { + expect(() => + getLoginCredential({ + usernameResourceName: "username_resource_id", + passwordResourceName: "password_resource_id", + }), + ).to.throw(FirebaseError, "Must specify username and password"); + }); + }); +}); diff --git a/src/appdistribution/options-parser-util.ts b/src/appdistribution/options-parser-util.ts index 632e7f7778b..dd42a68aba9 100644 --- a/src/appdistribution/options-parser-util.ts +++ b/src/appdistribution/options-parser-util.ts @@ -1,6 +1,7 @@ import * as fs from "fs-extra"; import { FirebaseError } from "../error"; import { needProjectNumber } from "../projectUtils"; +import { FieldHints, LoginCredential, TestDevice } from "./types"; /** * Takes in comma separated string or a path to a comma/new line separated file @@ -26,7 +27,7 @@ export function getTestersOrGroups(value: string, file: string): string[] { * returns a string[] of emails. */ export function getEmails(emails: string[], file: string): string[] { - if (emails.length == 0) { + if (emails.length === 0) { ensureFileExists(file); const readFile = fs.readFileSync(file, "utf8"); return splitter(readFile); @@ -48,6 +49,7 @@ function splitter(value: string): string[] { .map((entry) => entry.trim()) .filter((entry) => !!entry); } + // Gets project name from project number export async function getProjectName(options: any): Promise { const projectNumber = await needProjectNumber(options); @@ -62,3 +64,114 @@ export function getAppName(options: any): string { const appId = options.app; return `projects/${appId.split(":")[1]}/apps/${appId}`; } + +/** + * Takes in comma separated string or a path to a comma/new line separated file + * and converts the input into a string[] of test device strings. Value takes precedent + * over file. + */ +export function getTestDevices(value: string, file: string): TestDevice[] { + // If there is no value then the file gets parsed into a string to be split + if (!value && file) { + ensureFileExists(file); + value = fs.readFileSync(file, "utf8"); + } + + if (!value) { + return []; + } + + return value + .split(/[;\n]/) + .map((entry) => entry.trim()) + .filter((entry) => !!entry) + .map((str) => parseTestDevice(str)); +} + +function parseTestDevice(testDeviceString: string): TestDevice { + const entries = testDeviceString.split(","); + const allowedKeys = new Set(["model", "version", "orientation", "locale"]); + let model: string | undefined; + let version: string | undefined; + let orientation: string | undefined; + let locale: string | undefined; + for (const entry of entries) { + const keyAndValue = entry.split("="); + switch (keyAndValue[0]) { + case "model": + model = keyAndValue[1]; + break; + case "version": + version = keyAndValue[1]; + break; + case "orientation": + orientation = keyAndValue[1]; + break; + case "locale": + locale = keyAndValue[1]; + break; + default: + throw new FirebaseError( + `Unrecognized key in test devices. Can only contain ${Array.from(allowedKeys).join(", ")}`, + ); + } + } + + if (!model || !version || !orientation || !locale) { + throw new FirebaseError( + "Test devices must be in the format 'model=,version=,locale=,orientation='", + ); + } + return { model, version, locale, orientation }; +} + +/** + * Takes option values for username and password related options and returns a LoginCredential + * object that can be passed to the API. + */ +export function getLoginCredential(args: { + username?: string; + password?: string; + passwordFile?: string; + usernameResourceName?: string; + passwordResourceName?: string; +}): LoginCredential | undefined { + const { username, passwordFile, usernameResourceName, passwordResourceName } = args; + let password = args.password; + if (!password && passwordFile) { + ensureFileExists(passwordFile); + password = fs.readFileSync(passwordFile, "utf8").trim(); + } + + if (isPresenceMismatched(usernameResourceName, passwordResourceName)) { + throw new FirebaseError( + "Username and password resource names for automated tests need to be specified together.", + ); + } + let fieldHints: FieldHints | undefined; + if (usernameResourceName && passwordResourceName) { + fieldHints = { + usernameResourceName: usernameResourceName, + passwordResourceName: passwordResourceName, + }; + } + + if (isPresenceMismatched(username, password)) { + throw new FirebaseError( + "Username and password for automated tests need to be specified together.", + ); + } + let loginCredential: LoginCredential | undefined; + if (username && password) { + loginCredential = { username, password, fieldHints }; + } else if (fieldHints) { + throw new FirebaseError( + "Must specify username and password for automated tests if resource names are set", + ); + } + return loginCredential; +} + +function isPresenceMismatched(value1?: string, value2?: string) { + return (value1 && !value2) || (!value1 && value2); +} diff --git a/src/appdistribution/types.ts b/src/appdistribution/types.ts new file mode 100644 index 00000000000..224fe7bb5e9 --- /dev/null +++ b/src/appdistribution/types.ts @@ -0,0 +1,112 @@ +/** + * Helper interface for an app that is provisioned with App Distribution + */ +export interface AabInfo { + name: string; + integrationState: IntegrationState; + testCertificate: TestCertificate | null; +} + +export interface TestCertificate { + hashSha1: string; + hashSha256: string; + hashMd5: string; +} + +/** Enum representing the App Bundles state for the App */ +export enum IntegrationState { + AAB_INTEGRATION_STATE_UNSPECIFIED = "AAB_INTEGRATION_STATE_UNSPECIFIED", + INTEGRATED = "INTEGRATED", + PLAY_ACCOUNT_NOT_LINKED = "PLAY_ACCOUNT_NOT_LINKED", + NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT = "NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT", + APP_NOT_PUBLISHED = "APP_NOT_PUBLISHED", + AAB_STATE_UNAVAILABLE = "AAB_STATE_UNAVAILABLE", + PLAY_IAS_TERMS_NOT_ACCEPTED = "PLAY_IAS_TERMS_NOT_ACCEPTED", +} + +export enum UploadReleaseResult { + UPLOAD_RELEASE_RESULT_UNSPECIFIED = "UPLOAD_RELEASE_RESULT_UNSPECIFIED", + RELEASE_CREATED = "RELEASE_CREATED", + RELEASE_UPDATED = "RELEASE_UPDATED", + RELEASE_UNMODIFIED = "RELEASE_UNMODIFIED", +} + +export interface Release { + name: string; + releaseNotes: ReleaseNotes; + displayVersion: string; + buildVersion: string; + createTime: Date; + firebaseConsoleUri: string; + testingUri: string; + binaryDownloadUri: string; +} + +export interface ReleaseNotes { + text: string; +} + +export interface UploadReleaseResponse { + result: UploadReleaseResult; + release: Release; +} + +export interface BatchRemoveTestersResponse { + emails: string[]; +} + +export interface Group { + name: string; + displayName: string; + testerCount?: number; + releaseCount?: number; + inviteLinkCount?: number; +} + +export interface CreateReleaseTestRequest { + releaseTest: ReleaseTest; +} + +export interface TestDevice { + model: string; + version: string; + locale: string; + orientation: string; +} + +export type TestState = "IN_PROGRESS" | "PASSED" | "FAILED" | "INCONCLUSIVE"; + +export interface DeviceExecution { + device: TestDevice; + state?: TestState; + failedReason?: string; + inconclusiveReason?: string; +} + +export function mapDeviceToExecution(device: TestDevice): DeviceExecution { + return { + device: { + model: device.model, + version: device.version, + orientation: device.orientation, + locale: device.locale, + }, + }; +} + +export interface FieldHints { + usernameResourceName?: string; + passwordResourceName?: string; +} + +export interface LoginCredential { + username?: string; + password?: string; + fieldHints?: FieldHints; +} + +export interface ReleaseTest { + name?: string; + deviceExecutions: DeviceExecution[]; + loginCredential?: LoginCredential; +} diff --git a/src/apphosting/app.spec.ts b/src/apphosting/app.spec.ts new file mode 100644 index 00000000000..7f6c55039f3 --- /dev/null +++ b/src/apphosting/app.spec.ts @@ -0,0 +1,94 @@ +import { webApps } from "./app"; +import * as apps from "../management/apps"; +import * as sinon from "sinon"; +import { expect } from "chai"; +import { FirebaseError } from "../error"; + +describe("app", () => { + const projectId = "projectId"; + const backendId = "backendId"; + + let listFirebaseApps: sinon.SinonStub; + + describe("getOrCreateWebApp", () => { + let createWebApp: sinon.SinonStub; + + beforeEach(() => { + createWebApp = sinon.stub(apps, "createWebApp"); + listFirebaseApps = sinon.stub(apps, "listFirebaseApps"); + }); + + afterEach(() => { + createWebApp.restore(); + listFirebaseApps.restore(); + }); + + it("should create an app with backendId if no web apps exist yet", async () => { + listFirebaseApps.returns(Promise.resolve([])); + createWebApp.returns({ displayName: backendId, appId: "appId" }); + + await webApps.getOrCreateWebApp(projectId, null, backendId); + expect(createWebApp).calledWith(projectId, { displayName: backendId }); + }); + + it("throws error if given webApp doesn't exist in project", async () => { + listFirebaseApps.returns( + Promise.resolve([ + { displayName: "testWebApp", appId: "testWebAppId", platform: apps.AppPlatform.WEB }, + ]), + ); + + await expect( + webApps.getOrCreateWebApp(projectId, "nonExistentWebApp", backendId), + ).to.be.rejectedWith( + FirebaseError, + "The web app 'nonExistentWebApp' does not exist in project projectId", + ); + }); + + it("returns undefined if user has reached the app limit for their project", async () => { + listFirebaseApps.returns(Promise.resolve([])); + createWebApp.throws({ original: { status: 429 } }); + + const app = await webApps.getOrCreateWebApp(projectId, null, backendId); + expect(app).equal(undefined); + }); + }); + + describe("generateWebAppName", () => { + beforeEach(() => { + listFirebaseApps = sinon.stub(apps, "listFirebaseApps"); + }); + + afterEach(() => { + listFirebaseApps.restore(); + }); + + it("returns backendId if no such web app already exists", async () => { + listFirebaseApps.returns(Promise.resolve([])); + + const appName = await webApps.generateWebAppName(projectId, backendId); + expect(appName).equal(backendId); + }); + + it("returns backendId as appName with a unique id if app with backendId already exists", async () => { + listFirebaseApps.returns(Promise.resolve([{ displayName: backendId, appId: "1234" }])); + + const appName = await webApps.generateWebAppName(projectId, backendId); + expect(appName).equal(`${backendId}_1`); + }); + + it("returns appropriate unique id if app with backendId already exists", async () => { + listFirebaseApps.returns( + Promise.resolve([ + { displayName: backendId, appId: "1234" }, + { displayName: `${backendId}_1`, appId: "1234" }, + { displayName: `${backendId}_2`, appId: "1234" }, + ]), + ); + + const appName = await webApps.generateWebAppName(projectId, backendId); + expect(appName).equal(`${backendId}_3`); + }); + }); +}); diff --git a/src/apphosting/app.ts b/src/apphosting/app.ts new file mode 100644 index 00000000000..c897cfaa854 --- /dev/null +++ b/src/apphosting/app.ts @@ -0,0 +1,106 @@ +import { AppMetadata, AppPlatform, createWebApp, listFirebaseApps } from "../management/apps"; +import { FirebaseError } from "../error"; +import { logSuccess, logWarning } from "../utils"; + +const CREATE_NEW_FIREBASE_WEB_APP = "CREATE_NEW_WEB_APP"; +const CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP = "CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP"; + +export const webApps = { + CREATE_NEW_FIREBASE_WEB_APP, + CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP, + getOrCreateWebApp, + generateWebAppName, +}; + +type FirebaseWebApp = { name: string; id: string }; + +/** + * If firebaseWebAppId is provided and a matching web app exists, it is + * returned. If firebaseWebAppId is not provided, a new web app with the given + * backendId is created. + * @param projectId user's projectId + * @param firebaseWebAppId (optional) id of an existing Firebase web app + * @param backendId name of the app hosting backend + * @return app name and app id + */ +async function getOrCreateWebApp( + projectId: string, + firebaseWebAppId: string | null, + backendId: string, +): Promise { + const webAppsInProject = await listFirebaseApps(projectId, AppPlatform.WEB); + + if (firebaseWebAppId) { + const webApp = webAppsInProject.find((app) => app.appId === firebaseWebAppId); + if (webApp === undefined) { + throw new FirebaseError( + `The web app '${firebaseWebAppId}' does not exist in project ${projectId}`, + ); + } + + return { + name: webApp.displayName ?? webApp.appId, + id: webApp.appId, + }; + } + + const webAppName = await generateWebAppName(projectId, backendId); + + try { + const app = await createWebApp(projectId, { displayName: webAppName }); + logSuccess(`Created a new Firebase web app named "${webAppName}"`); + return { name: app.displayName, id: app.appId }; + } catch (e) { + if (isQuotaError(e)) { + logWarning( + "Unable to create a new web app, the project has reached the quota for Firebase apps. Navigate to your Firebase console to manage or delete a Firebase app to continue. ", + ); + return; + } + + throw new FirebaseError("Unable to create a Firebase web app", { + original: e instanceof Error ? e : undefined, + }); + } +} + +async function generateWebAppName(projectId: string, backendId: string): Promise { + const webAppsInProject = await listFirebaseApps(projectId, AppPlatform.WEB); + const appsMap = firebaseAppsToMap(webAppsInProject); + if (!appsMap.get(backendId)) { + return backendId; + } + + let uniqueId = 1; + let webAppName = `${backendId}_${uniqueId}`; + + while (appsMap.get(webAppName)) { + uniqueId += 1; + webAppName = `${backendId}_${uniqueId}`; + } + + return webAppName; +} + +function firebaseAppsToMap(apps: AppMetadata[]): Map { + return new Map( + apps.map((obj) => [ + // displayName can be null, use app id instead if so. Example - displayName: "mathusan-web-app", appId: "1:461896338144:web:426291191cccce65fede85" + obj.displayName ?? obj.appId, + obj.appId, + ]), + ); +} + +/** + * TODO: Make this generic to be re-used in other parts of the CLI + */ +function isQuotaError(error: any): boolean { + const original = error.original as any; + const code: number | undefined = + original?.status || + original?.context?.response?.statusCode || + original?.context?.body?.error?.code; + + return code === 429; +} diff --git a/src/apphosting/config.spec.ts b/src/apphosting/config.spec.ts new file mode 100644 index 00000000000..3cf062037ca --- /dev/null +++ b/src/apphosting/config.spec.ts @@ -0,0 +1,213 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as yaml from "yaml"; +import * as path from "path"; + +import * as fsImport from "../fsutils"; +import * as promptImport from "../prompt"; +import * as dialogs from "./secrets/dialogs"; +import * as config from "./config"; +import { NodeType } from "yaml/dist/nodes/Node"; + +describe("config", () => { + describe("yamlPath", () => { + let fs: sinon.SinonStubbedInstance; + + beforeEach(() => { + fs = sinon.stub(fsImport); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("finds apphosting.yaml at cwd", () => { + fs.fileExistsSync.withArgs("/cwd/apphosting.yaml").returns(true); + expect(config.yamlPath("/cwd")).equals("/cwd/apphosting.yaml"); + }); + + it("finds apphosting.yaml in a parent directory", () => { + fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false); + fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false); + fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(true); + + expect(config.yamlPath("/parent/cwd")).equals("/parent/apphosting.yaml"); + }); + + it("returns null if it finds firebase.json without finding apphosting.yaml", () => { + fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false); + fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false); + fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(false); + fs.fileExistsSync.withArgs("/parent/firebase.json").returns(true); + + expect(config.yamlPath("/parent/cwd")).equals(null); + }); + + it("returns if it reaches the fs root", () => { + fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false); + fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false); + fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(false); + fs.fileExistsSync.withArgs("/parent/firebase.json").returns(false); + fs.fileExistsSync.withArgs("/apphosting.yaml").returns(false); + fs.fileExistsSync.withArgs("/firebase.json").returns(false); + + expect(config.yamlPath("/parent/cwd")).equals(null); + }); + }); + + describe("get/setEnv", () => { + it("sets new envs", () => { + const doc = new yaml.Document>(); + const env: config.Env = { + variable: "VARIABLE", + value: "value", + }; + + config.upsertEnv(doc, env); + + const envAgain = config.findEnv(doc, env.variable); + expect(envAgain).deep.equals(env); + + // Also check raw YAML: + const envs = doc.get("env") as yaml.YAMLSeq; + expect(envs.toJSON()).to.deep.equal([env]); + }); + + it("overwrites envs", () => { + const doc = new yaml.Document>(); + const env: config.Env = { + variable: "VARIABLE", + value: "value", + }; + + const newEnv: config.Env = { + variable: env.variable, + secret: "my-secret", + }; + + config.upsertEnv(doc, env); + config.upsertEnv(doc, newEnv); + + expect(config.findEnv(doc, env.variable)).to.deep.equal(newEnv); + }); + + it("Preserves comments", () => { + const rawDoc = ` +# Run config +runConfig: + # Reserve capacity + minInstances: 1 + +env: + # Publicly available + - variable: NEXT_PUBLIC_BUCKET + value: mybucket.appspot.com +`.trim(); + + const expectedAmendments = ` + - variable: GOOGLE_API_KEY + secret: api-key +`; + + const doc = yaml.parseDocument(rawDoc) as yaml.Document>; + config.upsertEnv(doc, { + variable: "GOOGLE_API_KEY", + secret: "api-key", + }); + + expect(doc.toString()).to.equal(rawDoc + expectedAmendments); + }); + }); + + describe("maybeAddSecretToYaml", () => { + let prompt: sinon.SinonStubbedInstance; + let yamlPath: sinon.SinonStub; + let load: sinon.SinonStub; + let findEnv: sinon.SinonStub; + let upsertEnv: sinon.SinonStub; + let store: sinon.SinonStub; + let envVarForSecret: sinon.SinonStub; + + beforeEach(() => { + prompt = sinon.stub(promptImport); + yamlPath = sinon.stub(config, "yamlPath"); + load = sinon.stub(config, "load"); + findEnv = sinon.stub(config, "findEnv"); + upsertEnv = sinon.stub(config, "upsertEnv"); + store = sinon.stub(config, "store"); + envVarForSecret = sinon.stub(dialogs, "envVarForSecret"); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("noops if the env already exists", async () => { + const doc = yaml.parseDocument("{}"); + yamlPath.returns("CWD/apphosting.yaml"); + load.returns(doc); + findEnv.withArgs(doc, "SECRET").returns({ variable: "SECRET", secret: "SECRET" }); + + await config.maybeAddSecretToYaml("SECRET"); + + expect(yamlPath).to.have.been.called; + expect(load).to.have.been.calledWith("CWD/apphosting.yaml"); + expect(prompt.confirm).to.not.have.been.called; + expect(prompt.promptOnce).to.not.have.been.called; + }); + + it("inserts into an existing doc", async () => { + const doc = yaml.parseDocument("{}"); + yamlPath.returns("CWD/apphosting.yaml"); + load.withArgs(path.join("CWD", "apphosting.yaml")).returns(doc); + findEnv.withArgs(doc, "SECRET").returns(undefined); + prompt.confirm.resolves(true); + envVarForSecret.resolves("SECRET_VARIABLE"); + + await config.maybeAddSecretToYaml("SECRET"); + + expect(yamlPath).to.have.been.called; + expect(load).to.have.been.calledWith("CWD/apphosting.yaml"); + expect(prompt.confirm).to.have.been.calledWithMatch({ + message: "Would you like to add this secret to apphosting.yaml?", + default: true, + }); + expect(envVarForSecret).to.have.been.calledWith("SECRET"); + expect(upsertEnv).to.have.been.calledWithMatch(doc, { + variable: "SECRET_VARIABLE", + secret: "SECRET", + }); + expect(store).to.have.been.calledWithMatch(path.join("CWD", "apphosting.yaml"), doc); + expect(prompt.promptOnce).to.not.have.been.called; + }); + + it("inserts into an new doc", async () => { + const doc = new yaml.Document(); + yamlPath.returns(undefined); + findEnv.withArgs(doc, "SECRET").returns(undefined); + prompt.confirm.resolves(true); + prompt.promptOnce.resolves("CWD"); + envVarForSecret.resolves("SECRET_VARIABLE"); + + await config.maybeAddSecretToYaml("SECRET"); + + expect(yamlPath).to.have.been.called; + expect(load).to.not.have.been.called; + expect(prompt.confirm).to.have.been.calledWithMatch({ + message: "Would you like to add this secret to apphosting.yaml?", + default: true, + }); + expect(prompt.promptOnce).to.have.been.calledWithMatch({ + message: + "It looks like you don't have an apphosting.yaml yet. Where would you like to store it?", + default: process.cwd(), + }); + expect(envVarForSecret).to.have.been.calledWith("SECRET"); + expect(upsertEnv).to.have.been.calledWithMatch(doc, { + variable: "SECRET_VARIABLE", + secret: "SECRET", + }); + expect(store).to.have.been.calledWithMatch(path.join("CWD", "apphosting.yaml"), doc); + }); + }); +}); diff --git a/src/apphosting/config.ts b/src/apphosting/config.ts new file mode 100644 index 00000000000..deceb2d0ff1 --- /dev/null +++ b/src/apphosting/config.ts @@ -0,0 +1,155 @@ +import { resolve, join, dirname } from "path"; +import { writeFileSync } from "fs"; +import * as yaml from "yaml"; + +import * as fs from "../fsutils"; +import { NodeType } from "yaml/dist/nodes/Node"; +import * as prompt from "../prompt"; +import * as dialogs from "./secrets/dialogs"; + +export interface RunConfig { + concurrency?: number; + cpu?: number; + memoryMiB?: number; + minInstances?: number; + maxInstances?: number; +} + +/** Where an environment variable can be provided. */ +export type Availability = "BUILD" | "RUNTIME"; + +/** Config for an environment variable. */ +export type Env = { + variable: string; + secret?: string; + value?: string; + availability?: Availability[]; +}; + +/** Schema for apphosting.yaml. */ +export interface Config { + runConfig?: RunConfig; + env?: Env[]; +} + +/** + * Finds the path of apphosting.yaml. + * Starts with cwd and walks up the tree until apphosting.yaml is found or + * we find the project root (where firebase.json is) or the filesystem root; + * in these cases, returns null. + */ +export function yamlPath(cwd: string): string | null { + let dir = cwd; + + while (!fs.fileExistsSync(resolve(dir, "apphosting.yaml"))) { + // We've hit project root + if (fs.fileExistsSync(resolve(dir, "firebase.json"))) { + return null; + } + + const parent = dirname(dir); + // We've hit the filesystem root + if (parent === dir) { + return null; + } + dir = parent; + } + return resolve(dir, "apphosting.yaml"); +} + +/** Load apphosting.yaml */ +export function load(yamlPath: string): yaml.Document { + const raw = fs.readFile(yamlPath); + return yaml.parseDocument(raw); +} + +/** Save apphosting.yaml */ +export function store(yamlPath: string, document: yaml.Document): void { + writeFileSync(yamlPath, document.toString()); +} + +/** Gets the first Env with a given variable name. */ +export function findEnv(document: yaml.Document, variable: string): Env | undefined { + if (!document.has("env")) { + return undefined; + } + const envs = document.get("env") as yaml.YAMLSeq; + for (const env of envs.items as Array>) { + if ((env.get("variable") as unknown) === variable) { + return env.toJSON() as Env; + } + } + return undefined; +} + +/** Inserts or overwrites the first Env with the env.variable name. */ +export function upsertEnv(document: yaml.Document, env: Env): void { + if (!document.has("env")) { + document.set("env", document.createNode([env])); + return; + } + const envs = document.get("env") as yaml.YAMLSeq>; + + // The type system in this library is... not great at propagating type info + const envYaml = document.createNode(env); + for (let i = 0; i < envs.items.length; i++) { + if ((envs.items[i].get("variable") as unknown) === env.variable) { + // Note to reviewers: Should we instead set per each field so that we preserve comments? + envs.set(i, envYaml); + return; + } + } + + envs.add(envYaml); +} + +/** + * Given a secret name, guides the user whether they want to add that secret to apphosting.yaml. + * If an apphosting.yaml exists and includes the secret already is used as a variable name, exist early. + * If apphosting.yaml does not exist, offers to create it. + * If env does not exist, offers to add it. + * If secretName is not a valid env var name, prompts for an env var name. + */ +export async function maybeAddSecretToYaml(secretName: string): Promise { + // We must go through the exports object for stubbing to work in tests. + const dynamicDispatch = exports as { + yamlPath: typeof yamlPath; + load: typeof load; + findEnv: typeof findEnv; + upsertEnv: typeof upsertEnv; + store: typeof store; + }; + // Note: The API proposal suggested that we would check if the env exists. This is stupidly hard because the YAML may not exist yet. + let path = dynamicDispatch.yamlPath(process.cwd()); + let projectYaml: yaml.Document; + if (path) { + projectYaml = dynamicDispatch.load(path); + } else { + projectYaml = new yaml.Document(); + } + // TODO: Should we search for any env where it has secret: secretName rather than variable: secretName? + if (dynamicDispatch.findEnv(projectYaml, secretName)) { + return; + } + const addToYaml = await prompt.confirm({ + message: "Would you like to add this secret to apphosting.yaml?", + default: true, + }); + if (!addToYaml) { + return; + } + if (!path) { + path = await prompt.promptOnce({ + message: + "It looks like you don't have an apphosting.yaml yet. Where would you like to store it?", + default: process.cwd(), + }); + path = join(path, "apphosting.yaml"); + } + const envName = await dialogs.envVarForSecret(secretName); + dynamicDispatch.upsertEnv(projectYaml, { + variable: envName, + secret: secretName, + }); + dynamicDispatch.store(path, projectYaml); +} diff --git a/src/apphosting/constants.ts b/src/apphosting/constants.ts new file mode 100644 index 00000000000..87ebefb1bca --- /dev/null +++ b/src/apphosting/constants.ts @@ -0,0 +1,3 @@ +export const DEFAULT_LOCATION = "us-central1"; +export const DEFAULT_DEPLOY_METHOD = "github"; +export const ALLOWED_DEPLOY_METHODS = [{ name: "Deploy using github", value: "github" }]; diff --git a/src/apphosting/githubConnections.spec.ts b/src/apphosting/githubConnections.spec.ts new file mode 100644 index 00000000000..6bc8a868296 --- /dev/null +++ b/src/apphosting/githubConnections.spec.ts @@ -0,0 +1,398 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import * as prompt from "../prompt"; +import * as poller from "../operation-poller"; +import * as devconnect from "../gcp/devConnect"; +import * as repo from "./githubConnections"; +import * as utils from "../utils"; +import * as srcUtils from "../getProjectNumber"; +import * as rm from "../gcp/resourceManager"; +import { FirebaseError } from "../error"; + +const projectId = "projectId"; +const location = "us-central1"; + +function mockConn(id: string): devconnect.Connection { + return { + name: `projects/${projectId}/locations/${location}/connections/${id}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + }; +} + +function mockRepo(name: string): devconnect.GitRepositoryLink { + return { + name: `${name}`, + cloneUri: `https://github.com/test/${name}.git`, + createTime: "", + updateTime: "", + deleteTime: "", + reconciling: false, + uid: "", + }; +} + +describe("githubConnections", () => { + describe("parseConnectionName", () => { + it("should parse valid connection name", () => { + const connectionName = "projects/my-project/locations/us-central1/connections/my-conn"; + + const expected = { + projectId: "my-project", + location: "us-central1", + id: "my-conn", + }; + + expect(repo.parseConnectionName(connectionName)).to.deep.equal(expected); + }); + + it("should return undefined for invalid", () => { + expect( + repo.parseConnectionName( + "projects/my-project/locations/us-central1/connections/my-conn/repositories/repo", + ), + ).to.be.undefined; + expect(repo.parseConnectionName("foobar")).to.be.undefined; + }); + }); + + describe("extractRepoSlugFromUri", () => { + it("extracts repo from URI", () => { + const cloneUri = "https://github.com/user/repo.git"; + const repoSlug = repo.extractRepoSlugFromUri(cloneUri); + expect(repoSlug).to.equal("user/repo"); + }); + }); + + describe("generateRepositoryId", () => { + it("extracts repo from URI", () => { + const cloneUri = "https://github.com/user/repo.git"; + const repoSlug = repo.generateRepositoryId(cloneUri); + expect(repoSlug).to.equal("user-repo"); + }); + }); + + describe("connect GitHub repo", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let promptOnceStub: sinon.SinonStub; + let pollOperationStub: sinon.SinonStub; + let getConnectionStub: sinon.SinonStub; + let getRepositoryStub: sinon.SinonStub; + let createConnectionStub: sinon.SinonStub; + let serviceAccountHasRolesStub: sinon.SinonStub; + let createRepositoryStub: sinon.SinonStub; + let fetchLinkableRepositoriesStub: sinon.SinonStub; + let getProjectNumberStub: sinon.SinonStub; + let openInBrowserPopupStub: sinon.SinonStub; + + beforeEach(() => { + promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call"); + pollOperationStub = sandbox + .stub(poller, "pollOperation") + .throws("Unexpected pollOperation call"); + getConnectionStub = sandbox + .stub(devconnect, "getConnection") + .throws("Unexpected getConnection call"); + getRepositoryStub = sandbox + .stub(devconnect, "getGitRepositoryLink") + .throws("Unexpected getGitRepositoryLink call"); + createConnectionStub = sandbox + .stub(devconnect, "createConnection") + .throws("Unexpected createConnection call"); + serviceAccountHasRolesStub = sandbox.stub(rm, "serviceAccountHasRoles").resolves(true); + createRepositoryStub = sandbox + .stub(devconnect, "createGitRepositoryLink") + .throws("Unexpected createGitRepositoryLink call"); + fetchLinkableRepositoriesStub = sandbox + .stub(devconnect, "listAllLinkableGitRepositories") + .throws("Unexpected listAllLinkableGitRepositories call"); + sandbox.stub(utils, "openInBrowser").resolves(); + openInBrowserPopupStub = sandbox + .stub(utils, "openInBrowserPopup") + .throws("Unexpected openInBrowserPopup call"); + getProjectNumberStub = sandbox + .stub(srcUtils, "getProjectNumber") + .throws("Unexpected getProjectNumber call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + const connectionId = `apphosting-${location}`; + + const op = { + name: `projects/${projectId}/locations/${location}/connections/${connectionId}`, + done: true, + }; + const pendingConn = { + name: `projects/${projectId}/locations/${location}/connections/${connectionId}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "PENDING_USER_OAUTH", + message: "pending", + actionUri: "https://google.com", + }, + reconciling: false, + }; + const completeConn = { + name: `projects/${projectId}/locations/${location}/connections/${connectionId}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + }; + const repos = { + repositories: [ + { + name: "repo0", + remoteUri: "https://github.com/test/repo0.git", + }, + { + name: "repo1", + remoteUri: "https://github.com/test/repo1.git", + }, + ], + }; + + it("creates a connection if it doesn't exist", async () => { + getConnectionStub.onFirstCall().rejects(new FirebaseError("error", { status: 404 })); + getConnectionStub.onSecondCall().resolves(completeConn); + createConnectionStub.resolves(op); + pollOperationStub.resolves(pendingConn); + promptOnceStub.onFirstCall().resolves("any key"); + + await repo.getOrCreateConnection(projectId, location, connectionId); + expect(createConnectionStub).to.be.calledWith(projectId, location, connectionId); + }); + + it("checks if secret manager admin role is granted for developer connect P4SA when creating an oauth connection", async () => { + getConnectionStub.onFirstCall().rejects(new FirebaseError("error", { status: 404 })); + getConnectionStub.onSecondCall().resolves(completeConn); + createConnectionStub.resolves(op); + pollOperationStub.resolves(pendingConn); + promptOnceStub.resolves("any key"); + getProjectNumberStub.onFirstCall().resolves(projectId); + openInBrowserPopupStub.resolves({ url: "", cleanup: sandbox.stub() }); + + await repo.getOrCreateOauthConnection(projectId, location); + expect(serviceAccountHasRolesStub).to.be.calledWith( + projectId, + `service-${projectId}@gcp-sa-devconnect.iam.gserviceaccount.com`, + ["roles/secretmanager.admin"], + true, + ); + }); + + it("creates repository if it doesn't exist", async () => { + getConnectionStub.resolves(completeConn); + fetchLinkableRepositoriesStub.resolves(repos); + promptOnceStub.onFirstCall().resolves(repos.repositories[0].remoteUri); + getRepositoryStub.rejects(new FirebaseError("error", { status: 404 })); + createRepositoryStub.resolves({ name: "op" }); + pollOperationStub.resolves(repos.repositories[0]); + + await repo.getOrCreateRepository( + projectId, + location, + connectionId, + repos.repositories[0].remoteUri, + ); + expect(createRepositoryStub).to.be.calledWith( + projectId, + location, + connectionId, + "test-repo0", + repos.repositories[0].remoteUri, + ); + }); + + it("re-uses existing repository it already exists", async () => { + getConnectionStub.resolves(completeConn); + fetchLinkableRepositoriesStub.resolves(repos); + promptOnceStub.onFirstCall().resolves(repos.repositories[0].remoteUri); + getRepositoryStub.resolves(repos.repositories[0]); + + const r = await repo.getOrCreateRepository( + projectId, + location, + connectionId, + repos.repositories[0].remoteUri, + ); + expect(r).to.be.deep.equal(repos.repositories[0]); + }); + }); + + describe("fetchAllRepositories", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let listAllLinkableGitRepositoriesStub: sinon.SinonStub; + + beforeEach(() => { + listAllLinkableGitRepositoriesStub = sandbox + .stub(devconnect, "listAllLinkableGitRepositories") + .throws("Unexpected listAllLinkableGitRepositories call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("should fetch all linkable repositories from multiple connections", async () => { + const conn0 = mockConn("conn0"); + const conn1 = mockConn("conn1"); + const repo0 = mockRepo("repo-0"); + const repo1 = mockRepo("repo-1"); + listAllLinkableGitRepositoriesStub.onFirstCall().resolves([repo0]); + listAllLinkableGitRepositoriesStub.onSecondCall().resolves([repo1]); + + const { cloneUris, cloneUriToConnection } = await repo.fetchAllRepositories(projectId, [ + conn0, + conn1, + ]); + + expect(cloneUris.length).to.equal(2); + expect(cloneUriToConnection).to.deep.equal({ + [repo0.cloneUri]: conn0, + [repo1.cloneUri]: conn1, + }); + }); + + it("should fetch all linkable repositories without duplicates when there are duplicate connections", async () => { + const conn0 = mockConn("conn0"); + const conn1 = mockConn("conn1"); + const repo0 = mockRepo("repo-0"); + const repo1 = mockRepo("repo-1"); + listAllLinkableGitRepositoriesStub.onFirstCall().resolves([repo0, repo1]); + listAllLinkableGitRepositoriesStub.onSecondCall().resolves([repo0, repo1]); + + const { cloneUris, cloneUriToConnection } = await repo.fetchAllRepositories(projectId, [ + conn0, + conn1, + ]); + + expect(cloneUris.length).to.equal(2); + expect(cloneUriToConnection).to.deep.equal({ + [repo0.cloneUri]: conn1, + [repo1.cloneUri]: conn1, + }); + }); + }); + + describe("listAppHostingConnections", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let listConnectionsStub: sinon.SinonStub; + + function extractId(name: string): string { + const parts = name.split("/"); + return parts.pop() ?? ""; + } + + beforeEach(() => { + listConnectionsStub = sandbox + .stub(devconnect, "listAllConnections") + .throws("Unexpected listAllConnections call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("filters out non-apphosting connections", async () => { + listConnectionsStub.resolves([ + mockConn("apphosting-github-conn-baddcafe"), + mockConn("hooray-conn"), + mockConn("apphosting-github-conn-deadbeef"), + mockConn("apphosting-github-oauth"), + ]); + + const conns = await repo.listAppHostingConnections(projectId); + expect(conns).to.have.length(2); + expect(conns.map((c) => extractId(c.name))).to.include.members([ + "apphosting-github-conn-baddcafe", + "apphosting-github-conn-deadbeef", + ]); + }); + }); + + describe("ensureSecretManagerAdminGrant", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let promptOnceStub: sinon.SinonStub; + let serviceAccountHasRolesStub: sinon.SinonStub; + let addServiceAccountToRolesStub: sinon.SinonStub; + let generateP4SAStub: sinon.SinonStub; + + beforeEach(() => { + promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call"); + serviceAccountHasRolesStub = sandbox.stub(rm, "serviceAccountHasRoles"); + sandbox.stub(srcUtils, "getProjectNumber").resolves(projectId); + addServiceAccountToRolesStub = sandbox.stub(rm, "addServiceAccountToRoles"); + generateP4SAStub = sandbox.stub(devconnect, "generateP4SA"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("does not prompt user if the developer connect P4SA already has secretmanager.admin permissions", async () => { + serviceAccountHasRolesStub.resolves(true); + await repo.ensureSecretManagerAdminGrant(projectId); + + expect(serviceAccountHasRolesStub).calledWith( + projectId, + `service-${projectId}@gcp-sa-devconnect.iam.gserviceaccount.com`, + ["roles/secretmanager.admin"], + ); + expect(promptOnceStub).to.not.be.called; + }); + + it("prompts user if the developer connect P4SA does not have secretmanager.admin permissions", async () => { + serviceAccountHasRolesStub.resolves(false); + promptOnceStub.resolves(true); + addServiceAccountToRolesStub.resolves(); + + await repo.ensureSecretManagerAdminGrant(projectId); + + expect(serviceAccountHasRolesStub).calledWith( + projectId, + `service-${projectId}@gcp-sa-devconnect.iam.gserviceaccount.com`, + ["roles/secretmanager.admin"], + ); + + expect(promptOnceStub).to.be.called; + }); + + it("tries to generate developer connect P4SA if adding role throws an error", async () => { + serviceAccountHasRolesStub.resolves(false); + promptOnceStub.resolves(true); + generateP4SAStub.resolves(); + addServiceAccountToRolesStub.onFirstCall().throws({ code: 400, status: 400 }); + addServiceAccountToRolesStub.onSecondCall().resolves(); + + await repo.ensureSecretManagerAdminGrant(projectId); + + expect(serviceAccountHasRolesStub).calledWith( + projectId, + `service-${projectId}@gcp-sa-devconnect.iam.gserviceaccount.com`, + ["roles/secretmanager.admin"], + ).calledOnce; + expect(generateP4SAStub).calledOnce; + expect(promptOnceStub).to.be.called; + }); + }); +}); diff --git a/src/apphosting/githubConnections.ts b/src/apphosting/githubConnections.ts new file mode 100644 index 00000000000..6a427b4b402 --- /dev/null +++ b/src/apphosting/githubConnections.ts @@ -0,0 +1,445 @@ +import * as clc from "colorette"; + +import * as devConnect from "../gcp/devConnect"; +import * as rm from "../gcp/resourceManager"; +import * as poller from "../operation-poller"; +import * as utils from "../utils"; +import { FirebaseError } from "../error"; +import { promptOnce } from "../prompt"; +import { getProjectNumber } from "../getProjectNumber"; +import { developerConnectOrigin } from "../api"; + +import * as fuzzy from "fuzzy"; +import * as inquirer from "inquirer"; + +interface ConnectionNameParts { + projectId: string; + location: string; + id: string; +} + +// Note: This does not match the sentinel oauth connection +const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/; +const APPHOSTING_OAUTH_CONN_NAME = "apphosting-github-oauth"; +const CONNECTION_NAME_REGEX = + /^projects\/(?[^\/]+)\/locations\/(?[^\/]+)\/connections\/(?[^\/]+)$/; + +/** + * Exported for unit testing. + * + * Example: /projects/my-project/locations/us-central1/connections/my-connection-id => { + * projectId: "my-project", + * location: "us-central1", + * id: "my-connection-id", + * } + */ +export function parseConnectionName(name: string): ConnectionNameParts | undefined { + const match = CONNECTION_NAME_REGEX.exec(name); + + if (!match || typeof match.groups === undefined) { + return; + } + const { projectId, location, id } = match.groups as unknown as ConnectionNameParts; + return { + projectId, + location, + id, + }; +} + +const devConnectPollerOptions: Omit = { + apiOrigin: developerConnectOrigin(), + apiVersion: "v1", + masterTimeout: 25 * 60 * 1_000, + maxBackoff: 10_000, +}; + +/** + * Exported for unit testing. + * + * Example usage: + * extractRepoSlugFromURI("https://github.com/user/repo.git") => "user/repo" + */ +export function extractRepoSlugFromUri(cloneUri: string): string | undefined { + const match = /github.com\/(.+).git/.exec(cloneUri); + if (!match) { + return undefined; + } + return match[1]; +} + +/** + * Exported for unit testing. + * + * Generates a repository ID. + * The relation is 1:* between Developer Connect Connection and GitHub Repositories. + */ +export function generateRepositoryId(remoteUri: string): string | undefined { + return extractRepoSlugFromUri(remoteUri)?.replaceAll("/", "-"); +} + +/** + * Generates connection id that matches specific id format recognized by all Firebase clients. + */ +function generateConnectionId(): string { + const randomHash = Math.random().toString(36).slice(6); + return `apphosting-github-conn-${randomHash}`; +} + +const ADD_CONN_CHOICE = "@ADD_CONN"; + +/** + * Prompts the user to link their backend to a GitHub repository. + */ +export async function linkGitHubRepository( + projectId: string, + location: string, +): Promise { + utils.logBullet(clc.bold(`${clc.yellow("===")} Import a GitHub repository`)); + // Fetch the sentinel Oauth connection first which is needed to create further GitHub connections. + const oauthConn = await getOrCreateOauthConnection(projectId, location); + const existingConns = await listAppHostingConnections(projectId); + + if (existingConns.length === 0) { + existingConns.push( + await createFullyInstalledConnection(projectId, location, generateConnectionId(), oauthConn), + ); + } + + let repoCloneUri: string | undefined; + let connection: devConnect.Connection; + do { + if (repoCloneUri === ADD_CONN_CHOICE) { + existingConns.push( + await createFullyInstalledConnection( + projectId, + location, + generateConnectionId(), + oauthConn, + /** withNewInstallation= */ true, + ), + ); + } + + const selection = await promptCloneUri(projectId, existingConns); + repoCloneUri = selection.cloneUri; + connection = selection.connection; + } while (repoCloneUri === ADD_CONN_CHOICE); + + // Ensure that the selected connection exists in the same region as the backend + const { id: connectionId } = parseConnectionName(connection.name)!; + await getOrCreateConnection(projectId, location, connectionId, { + authorizerCredential: connection.githubConfig?.authorizerCredential, + appInstallationId: connection.githubConfig?.appInstallationId, + }); + + const repo = await getOrCreateRepository(projectId, location, connectionId, repoCloneUri); + return repo; +} + +/** + * Creates a new DevConnect GitHub connection resource and ensures that it is fully configured on the GitHub + * side (ie associated with an account/org and some subset of repos within that scope). + * Copies over Oauth creds from the sentinel Oauth connection to save the user from having to + * reauthenticate with GitHub. + * @param projectId user's Firebase projectID + * @param location region where backend is being created + * @param connectionId id of connection to be created + * @param oauthConn user's oauth connection + * @param withNewInstallation Defaults to false if not set, and the Oauth connection's + * Installation Id is re-used when creating a new connection. + * If true the Oauth connection's installation Id is not re-used. + */ +async function createFullyInstalledConnection( + projectId: string, + location: string, + connectionId: string, + oauthConn: devConnect.Connection, + withNewInstallation = false, +): Promise { + let conn = await createConnection(projectId, location, connectionId, { + appInstallationId: withNewInstallation ? undefined : oauthConn.githubConfig?.appInstallationId, + authorizerCredential: oauthConn.githubConfig?.authorizerCredential, + }); + + while (conn.installationState.stage !== "COMPLETE") { + utils.logBullet( + "Install the Firebase App Hosting GitHub app to enable access to GitHub repositories", + ); + const targetUri = conn.installationState.actionUri; + utils.logBullet(targetUri); + await utils.openInBrowser(targetUri); + await promptOnce({ + type: "input", + message: + "Press Enter once you have installed or configured the Firebase App Hosting GitHub app to access your GitHub repo.", + }); + conn = await devConnect.getConnection(projectId, location, connectionId); + } + + return conn; +} + +/** + * Gets or creates the sentinel GitHub connection resource that contains our Firebase-wide GitHub Oauth token. + * This Oauth token can be used to create other connections without reprompting the user to grant access. + */ +export async function getOrCreateOauthConnection( + projectId: string, + location: string, +): Promise { + let conn: devConnect.Connection; + try { + conn = await devConnect.getConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME); + } catch (err: unknown) { + if ((err as any).status === 404) { + // Cloud build P4SA requires the secret manager admin role. + // This is required when creating an initial connection which is the Oauth connection in our case. + await ensureSecretManagerAdminGrant(projectId); + conn = await createConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME); + } else { + throw err; + } + } + + while (conn.installationState.stage === "PENDING_USER_OAUTH") { + utils.logBullet("Please authorize the Firebase GitHub app by visiting this url:"); + const { url, cleanup } = await utils.openInBrowserPopup( + conn.installationState.actionUri, + "Authorize the GitHub app", + ); + utils.logBullet(`\t${url}`); + await promptOnce({ + type: "input", + message: "Press Enter once you have authorized the GitHub App.", + }); + cleanup(); + const { projectId, location, id } = parseConnectionName(conn.name)!; + conn = await devConnect.getConnection(projectId, location, id); + } + utils.logSuccess("Connected with GitHub successfully\n"); + + return conn; +} + +async function promptCloneUri( + projectId: string, + connections: devConnect.Connection[], +): Promise<{ cloneUri: string; connection: devConnect.Connection }> { + const { cloneUris, cloneUriToConnection } = await fetchAllRepositories(projectId, connections); + const cloneUri = await promptOnce({ + type: "autocomplete", + name: "cloneUri", + message: "Which GitHub repo do you want to deploy?", + source: (_: any, input = ""): Promise<(inquirer.DistinctChoice | inquirer.Separator)[]> => { + return new Promise((resolve) => + resolve([ + new inquirer.Separator(), + { + name: "Missing a repo? Select this option to configure your GitHub connection settings", + value: ADD_CONN_CHOICE, + }, + new inquirer.Separator(), + ...fuzzy + .filter(input, cloneUris, { + extract: (uri) => extractRepoSlugFromUri(uri) || "", + }) + .map((result) => { + return { + name: extractRepoSlugFromUri(result.original) || "", + value: result.original, + }; + }), + ]), + ); + }, + }); + return { cloneUri, connection: cloneUriToConnection[cloneUri] }; +} + +/** + * Exported for unit testing + */ +export async function ensureSecretManagerAdminGrant(projectId: string): Promise { + const projectNumber = await getProjectNumber({ projectId }); + const dcsaEmail = devConnect.serviceAgentEmail(projectNumber); + + // will return false even if the service account does not exist in the project + const alreadyGranted = await rm.serviceAccountHasRoles( + projectId, + dcsaEmail, + ["roles/secretmanager.admin"], + true, + ); + if (alreadyGranted) { + utils.logBullet("secret manager admin role already granted"); + return; + } + + utils.logBullet( + "To create a new GitHub connection, Secret Manager Admin role (roles/secretmanager.admin) is required on the Developer Connect Service Agent.", + ); + const grant = await promptOnce({ + type: "confirm", + message: "Grant the required role to the Developer Connect Service Agent?", + }); + if (!grant) { + utils.logBullet( + "You, or your project administrator, should run the following command to grant the required role:\n\n" + + "You, or your project adminstrator, can run the following command to grant the required role manually:\n\n" + + `\tgcloud projects add-iam-policy-binding ${projectId} \\\n` + + `\t --member="serviceAccount:${dcsaEmail} \\\n` + + `\t --role="roles/secretmanager.admin\n`, + ); + throw new FirebaseError("Insufficient IAM permissions to create a new connection to GitHub"); + } + + try { + await rm.addServiceAccountToRoles( + projectId, + dcsaEmail, + ["roles/secretmanager.admin"], + /* skipAccountLookup= */ true, + ); + } catch (e: any) { + // if the dev connect P4SA doesn't exist in the project, generate one + if (e?.code === 400 || e?.status === 400) { + await devConnect.generateP4SA(projectNumber); + await rm.addServiceAccountToRoles( + projectId, + dcsaEmail, + ["roles/secretmanager.admin"], + /* skipAccountLookup= */ true, + ); + } else { + throw e; + } + } + + utils.logSuccess( + "Successfully granted the required role to the Developer Connect Service Agent!\n", + ); +} + +/** + * Creates a new Developer Connect Connection resource. Will typically need some initialization + * or configuration after being created. + */ +export async function createConnection( + projectId: string, + location: string, + connectionId: string, + githubConfig?: devConnect.GitHubConfig, +): Promise { + const op = await devConnect.createConnection(projectId, location, connectionId, githubConfig); + const conn = await poller.pollOperation({ + ...devConnectPollerOptions, + pollerName: `create-${location}-${connectionId}`, + operationResourceName: op.name, + }); + return conn; +} + +/** + * Exported for unit testing. + */ +export async function getOrCreateConnection( + projectId: string, + location: string, + connectionId: string, + githubConfig?: devConnect.GitHubConfig, +): Promise { + let conn: devConnect.Connection; + try { + conn = await devConnect.getConnection(projectId, location, connectionId); + } catch (err: unknown) { + if ((err as any).status === 404) { + utils.logBullet("creating connection"); + conn = await createConnection(projectId, location, connectionId, githubConfig); + } else { + throw err; + } + } + return conn; +} + +/** + * Exported for unit testing. + */ +export async function getOrCreateRepository( + projectId: string, + location: string, + connectionId: string, + cloneUri: string, +): Promise { + const repositoryId = generateRepositoryId(cloneUri); + if (!repositoryId) { + throw new FirebaseError(`Failed to generate repositoryId for URI "${cloneUri}".`); + } + let repo: devConnect.GitRepositoryLink; + try { + repo = await devConnect.getGitRepositoryLink(projectId, location, connectionId, repositoryId); + } catch (err: unknown) { + if ((err as FirebaseError).status === 404) { + const op = await devConnect.createGitRepositoryLink( + projectId, + location, + connectionId, + repositoryId, + cloneUri, + ); + repo = await poller.pollOperation({ + ...devConnectPollerOptions, + pollerName: `create-${location}-${connectionId}-${repositoryId}`, + operationResourceName: op.name, + }); + } else { + throw err; + } + } + return repo; +} + +/** + * Exported for unit testing. + * + * Lists all App Hosting Developer Connect Connections + * not including the OAuth Connection + */ +export async function listAppHostingConnections( + projectId: string, +): Promise { + const conns = await devConnect.listAllConnections(projectId, "-"); + return conns.filter( + (conn) => + APPHOSTING_CONN_PATTERN.test(conn.name) && + conn.installationState.stage === "COMPLETE" && + !conn.disabled, + ); +} + +/** + * Exported for unit testing. + */ +export async function fetchAllRepositories( + projectId: string, + connections: devConnect.Connection[], +): Promise<{ + cloneUris: string[]; + cloneUriToConnection: Record; +}> { + const cloneUriToConnection: Record = {}; + + for (const conn of connections) { + const { location, id } = parseConnectionName(conn.name)!; + const connectionRepos = await devConnect.listAllLinkableGitRepositories( + projectId, + location, + id, + ); + connectionRepos.forEach((repo) => { + cloneUriToConnection[repo.cloneUri] = conn; + }); + } + return { cloneUris: Object.keys(cloneUriToConnection), cloneUriToConnection }; +} diff --git a/src/apphosting/index.spec.ts b/src/apphosting/index.spec.ts new file mode 100644 index 00000000000..b4d0c09d027 --- /dev/null +++ b/src/apphosting/index.spec.ts @@ -0,0 +1,336 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as prompt from "../prompt"; +import * as apphosting from "../gcp/apphosting"; +import * as iam from "../gcp/iam"; +import * as resourceManager from "../gcp/resourceManager"; +import * as poller from "../operation-poller"; +import { + createBackend, + deleteBackendAndPoll, + promptLocation, + setDefaultTrafficPolicy, + ensureAppHostingComputeServiceAccount, + getBackendForAmbiguousLocation, +} from "./index"; +import * as deploymentTool from "../deploymentTool"; +import { FirebaseError } from "../error"; + +describe("apphosting setup functions", () => { + const projectId = "projectId"; + const location = "us-central1"; + const backendId = "backendId"; + + let promptOnceStub: sinon.SinonStub; + let pollOperationStub: sinon.SinonStub; + let createBackendStub: sinon.SinonStub; + let listBackendsStub: sinon.SinonStub; + let deleteBackendStub: sinon.SinonStub; + let updateTrafficStub: sinon.SinonStub; + let listLocationsStub: sinon.SinonStub; + let createServiceAccountStub: sinon.SinonStub; + let addServiceAccountToRolesStub: sinon.SinonStub; + let testResourceIamPermissionsStub: sinon.SinonStub; + + beforeEach(() => { + promptOnceStub = sinon.stub(prompt, "promptOnce").throws("Unexpected promptOnce call"); + pollOperationStub = sinon.stub(poller, "pollOperation").throws("Unexpected pollOperation call"); + createBackendStub = sinon + .stub(apphosting, "createBackend") + .throws("Unexpected createBackend call"); + listBackendsStub = sinon + .stub(apphosting, "listBackends") + .throws("Unexpected listBackends call"); + deleteBackendStub = sinon + .stub(apphosting, "deleteBackend") + .throws("Unexpected deleteBackend call"); + updateTrafficStub = sinon + .stub(apphosting, "updateTraffic") + .throws("Unexpected updateTraffic call"); + listLocationsStub = sinon + .stub(apphosting, "listLocations") + .throws("Unexpected listLocations call"); + createServiceAccountStub = sinon + .stub(iam, "createServiceAccount") + .throws("Unexpected createServiceAccount call"); + addServiceAccountToRolesStub = sinon + .stub(resourceManager, "addServiceAccountToRoles") + .throws("Unexpected addServiceAccountToRoles call"); + testResourceIamPermissionsStub = sinon + .stub(iam, "testResourceIamPermissions") + .throws("Unexpected testResourceIamPermissions call"); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + describe("createBackend", () => { + const webAppId = "webAppId"; + + const op = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}`, + done: true, + }; + + const completeBackend = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const cloudBuildConnRepo = { + name: `projects/${projectId}/locations/${location}/connections/framework-${location}/repositories/repoId`, + cloneUri: "cloneUri", + createTime: "0", + updateTime: "1", + deleteTime: "2", + reconciling: true, + uid: "1", + }; + + it("should create a new backend", async () => { + createBackendStub.resolves(op); + pollOperationStub.resolves(completeBackend); + + await createBackend( + projectId, + location, + backendId, + cloudBuildConnRepo, + "custom-service-account", + webAppId, + ); + + const backendInput: Omit = { + servingLocality: "GLOBAL_ACCESS", + codebase: { + repository: cloudBuildConnRepo.name, + rootDirectory: "/", + }, + labels: deploymentTool.labels(), + serviceAccount: "custom-service-account", + appId: webAppId, + }; + expect(createBackendStub).to.be.calledWith(projectId, location, backendInput); + }); + + it("should set default rollout policy to 100% all at once", async () => { + const completeTraffic: apphosting.Traffic = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`, + current: { splits: [] }, + reconciling: false, + createTime: "0", + updateTime: "1", + etag: "", + uid: "", + }; + updateTrafficStub.resolves(op); + pollOperationStub.resolves(completeTraffic); + + await setDefaultTrafficPolicy(projectId, location, backendId, "main"); + + expect(updateTrafficStub).to.be.calledWith(projectId, location, backendId, { + rolloutPolicy: { + codebaseBranch: "main", + stages: [ + { + progression: "IMMEDIATE", + targetPercent: 100, + }, + ], + }, + }); + }); + }); + + describe("ensureAppHostingComputeServiceAccount", () => { + const serviceAccount = "hello@example.com"; + + it("should succeed if the user has permissions for the service account", async () => { + testResourceIamPermissionsStub.resolves(); + + await expect(ensureAppHostingComputeServiceAccount(projectId, serviceAccount)).to.be + .fulfilled; + + expect(testResourceIamPermissionsStub).to.be.calledOnce; + expect(createServiceAccountStub).to.not.be.called; + expect(addServiceAccountToRolesStub).to.not.be.called; + }); + + it("should succeed if the user can create the service account when it does not exist", async () => { + testResourceIamPermissionsStub.rejects( + new FirebaseError("Permission denied", { status: 404 }), + ); + createServiceAccountStub.resolves(); + addServiceAccountToRolesStub.resolves(); + + await expect(ensureAppHostingComputeServiceAccount(projectId, serviceAccount)).to.be + .fulfilled; + + expect(testResourceIamPermissionsStub).to.be.calledOnce; + expect(createServiceAccountStub).to.be.calledOnce; + expect(addServiceAccountToRolesStub).to.be.calledOnce; + }); + + it("should throw an error if the user does not have permissions", async () => { + testResourceIamPermissionsStub.rejects( + new FirebaseError("Permission denied", { status: 403 }), + ); + + await expect( + ensureAppHostingComputeServiceAccount(projectId, serviceAccount), + ).to.be.rejectedWith(/Failed to create backend due to missing delegation permissions/); + + expect(testResourceIamPermissionsStub).to.be.calledOnce; + expect(createServiceAccountStub).to.not.be.called; + expect(addServiceAccountToRolesStub).to.not.be.called; + }); + + it("should throw the error if the user cannot create the service account", async () => { + testResourceIamPermissionsStub.rejects( + new FirebaseError("Permission denied", { status: 404 }), + ); + createServiceAccountStub.rejects(new FirebaseError("failed to create SA")); + + await expect( + ensureAppHostingComputeServiceAccount(projectId, serviceAccount), + ).to.be.rejectedWith("failed to create SA"); + + expect(testResourceIamPermissionsStub).to.be.calledOnce; + expect(createServiceAccountStub).to.be.calledOnce; + expect(addServiceAccountToRolesStub).to.not.be.called; + }); + }); + + describe("deleteBackendAndPoll", () => { + it("should delete a backend", async () => { + const op = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}`, + done: true, + }; + + deleteBackendStub.resolves(op); + pollOperationStub.resolves(); + + await deleteBackendAndPoll(projectId, location, backendId); + expect(deleteBackendStub).to.be.calledWith(projectId, location, backendId); + }); + }); + + describe("promptLocation", () => { + const supportedLocations = [ + { name: "us-central1", locationId: "us-central1" }, + { name: "us-west1", locationId: "us-west1" }, + ]; + + beforeEach(() => { + listLocationsStub.returns(supportedLocations); + promptOnceStub.returns(supportedLocations[0].locationId); + }); + + it("returns a location selection", async () => { + const location = await promptLocation(projectId, /* prompt= */ ""); + expect(location).to.be.eq("us-central1"); + }); + + it("uses a default location prompt if none is provided", async () => { + await promptLocation(projectId); + + expect(promptOnceStub).to.be.calledWith({ + name: "location", + type: "list", + default: "us-central1", + message: "Please select a location:", + choices: ["us-central1", "us-west1"], + }); + }); + + it("uses a custom location prompt if provided", async () => { + await promptLocation(projectId, "Custom location prompt:"); + + expect(promptOnceStub).to.be.calledWith({ + name: "location", + type: "list", + default: "us-central1", + message: "Custom location prompt:", + choices: ["us-central1", "us-west1"], + }); + }); + + it("skips the prompt if there's only 1 valid location choice", async () => { + listLocationsStub.returns(supportedLocations.slice(0, 1)); + + await expect(promptLocation(projectId, "Custom location prompt:")).to.eventually.equal( + supportedLocations[0].locationId, + ); + + expect(promptOnceStub).to.not.be.called; + }); + }); + + describe("getBackendForAmbiguousLocation", () => { + const backendFoo = { + name: `projects/${projectId}/locations/${location}/backends/foo`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const backendFooOtherRegion = { + name: `projects/${projectId}/locations/otherRegion/backends/foo`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const backendBar = { + name: `projects/${projectId}/locations/${location}/backends/bar`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + it("throws if there are no matching backends", async () => { + listBackendsStub.resolves({ backends: [] }); + + await expect( + getBackendForAmbiguousLocation(projectId, "baz", /* prompt= */ ""), + ).to.be.rejectedWith(/No backend named "baz" found./); + }); + + it("returns unambiguous backend", async () => { + listBackendsStub.resolves({ backends: [backendFoo, backendBar] }); + + await expect( + getBackendForAmbiguousLocation(projectId, "foo", /* prompt= */ ""), + ).to.eventually.equal(backendFoo); + }); + + it("prompts for location if backend is ambiguous", async () => { + listBackendsStub.resolves({ backends: [backendFoo, backendFooOtherRegion, backendBar] }); + promptOnceStub.resolves(location); + + await expect( + getBackendForAmbiguousLocation( + projectId, + "foo", + "Please select the location of the backend you'd like to delete:", + ), + ).to.eventually.equal(backendFoo); + + expect(promptOnceStub).to.be.calledWith({ + name: "location", + type: "list", + message: "Please select the location of the backend you'd like to delete:", + choices: [location, "otherRegion"], + }); + }); + }); +}); diff --git a/src/apphosting/index.ts b/src/apphosting/index.ts new file mode 100644 index 00000000000..5608a8b1249 --- /dev/null +++ b/src/apphosting/index.ts @@ -0,0 +1,502 @@ +import * as clc from "colorette"; +import * as poller from "../operation-poller"; +import * as apphosting from "../gcp/apphosting"; +import * as githubConnections from "./githubConnections"; +import { logBullet, logSuccess, logWarning, sleep } from "../utils"; +import { + apphostingOrigin, + artifactRegistryDomain, + cloudRunApiOrigin, + cloudbuildOrigin, + consoleOrigin, + developerConnectOrigin, + iamOrigin, + secretManagerOrigin, +} from "../api"; +import { Backend, BackendOutputOnlyFields, API_VERSION, Build, Rollout } from "../gcp/apphosting"; +import { addServiceAccountToRoles } from "../gcp/resourceManager"; +import * as iam from "../gcp/iam"; +import { FirebaseError } from "../error"; +import { promptOnce } from "../prompt"; +import { DEFAULT_LOCATION } from "./constants"; +import { ensure } from "../ensureApiEnabled"; +import * as deploymentTool from "../deploymentTool"; +import { DeepOmit } from "../metaprogramming"; +import { webApps } from "./app"; +import { GitRepositoryLink } from "../gcp/devConnect"; +import * as ora from "ora"; +import fetch from "node-fetch"; + +const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute"; + +const apphostingPollerOptions: Omit = { + apiOrigin: apphostingOrigin(), + apiVersion: API_VERSION, + masterTimeout: 25 * 60 * 1_000, + maxBackoff: 10_000, +}; + +async function tlsReady(url: string): Promise { + // Note, we do not use the helper libraries because they impose additional logic on content type and parsing. + try { + await fetch(url); + return true; + } catch (err) { + // At the time of this writing, the error code is ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE. + // I've chosen to use a regexp in an attempt to be forwards compatible with new versions of + // SSL. + const maybeNodeError = err as { cause: { code: string } }; + if (/HANDSHAKE_FAILURE/.test(maybeNodeError?.cause?.code)) { + return false; + } + return true; + } +} + +async function awaitTlsReady(url: string): Promise { + let ready; + do { + ready = await tlsReady(url); + if (!ready) { + await sleep(1000 /* ms */); + } + } while (!ready); +} + +/** + * Set up a new App Hosting backend. + */ +export async function doSetup( + projectId: string, + webAppName: string | null, + location: string | null, + serviceAccount: string | null, +): Promise { + await Promise.all([ + ensure(projectId, developerConnectOrigin(), "apphosting", true), + ensure(projectId, cloudbuildOrigin(), "apphosting", true), + ensure(projectId, secretManagerOrigin(), "apphosting", true), + ensure(projectId, cloudRunApiOrigin(), "apphosting", true), + ensure(projectId, artifactRegistryDomain(), "apphosting", true), + ensure(projectId, iamOrigin(), "apphosting", true), + ]); + + // Hack: Because IAM can take ~45 seconds to propagate, we provision the service account as soon as + // possible to reduce the likelihood that the subsequent Cloud Build fails. See b/336862200. + await ensureAppHostingComputeServiceAccount(projectId, serviceAccount); + + const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId); + if (location) { + if (!allowedLocations.includes(location)) { + throw new FirebaseError( + `Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`, + ); + } + } + + location = + location || (await promptLocation(projectId, "Select a location to host your backend:\n")); + + const gitRepositoryConnection: GitRepositoryLink = await githubConnections.linkGitHubRepository( + projectId, + location, + ); + + const rootDir = await promptOnce({ + name: "rootDir", + type: "input", + default: "/", + message: "Specify your app's root directory relative to your repository", + }); + + // TODO: Once tag patterns are implemented, prompt which method the user + // prefers. We could reduce the number of questions asked by letting people + // enter tag:? + const branch = await promptOnce({ + name: "branch", + type: "input", + default: "main", + message: "Pick a branch for continuous deployment", + }); + logSuccess(`Repo linked successfully!\n`); + + logBullet(`${clc.yellow("===")} Set up your backend`); + const backendId = await promptNewBackendId(projectId, location, { + name: "backendId", + type: "input", + default: "my-web-app", + message: "Provide a name for your backend [1-30 characters]", + }); + logSuccess(`Name set to ${backendId}\n`); + + const webApp = await webApps.getOrCreateWebApp(projectId, webAppName, backendId); + if (!webApp) { + logWarning(`Firebase web app not set`); + } + + const createBackendSpinner = ora("Creating your new backend...").start(); + const backend = await createBackend( + projectId, + location, + backendId, + gitRepositoryConnection, + serviceAccount, + webApp?.id, + rootDir, + ); + createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`); + + await setDefaultTrafficPolicy(projectId, location, backendId, branch); + + const confirmRollout = await promptOnce({ + type: "confirm", + name: "rollout", + default: true, + message: "Do you want to deploy now?", + }); + + if (!confirmRollout) { + logSuccess(`Your backend will be deployed at:\n\thttps://${backend.uri}`); + return; + } + + const url = `https://${backend.uri}`; + logBullet( + `You may also track this rollout at:\n\t${consoleOrigin()}/project/${projectId}/apphosting`, + ); + // TODO: Previous versions of this command printed the URL before the rollout started so that + // if a user does exit they will know where to go later. Should this be re-added? + const createRolloutSpinner = ora( + "Starting a new rollout; this may take a few minutes. It's safe to exit now.", + ).start(); + await orchestrateRollout(projectId, location, backendId, { + source: { + codebase: { + branch, + }, + }, + }); + createRolloutSpinner.succeed("Rollout complete"); + if (!(await tlsReady(url))) { + const tlsSpinner = ora( + "Finalizing your backend's TLS certificate; this may take a few minutes.", + ).start(); + await awaitTlsReady(url); + tlsSpinner.succeed("TLS certificate ready"); + } + logSuccess(`Your backend is now deployed at:\n\thttps://${backend.uri}`); +} + +/** + * Ensures the service account is present the user has permissions to use it by + * checking the `iam.serviceAccounts.actAs` permission. If the permissions + * check fails, this returns an error. If the permission check fails with a + * "not found" error, this attempts to provision the service account. + */ +export async function ensureAppHostingComputeServiceAccount( + projectId: string, + serviceAccount: string | null, +): Promise { + const sa = serviceAccount || defaultComputeServiceAccountEmail(projectId); + const name = `projects/${projectId}/serviceAccounts/${sa}`; + try { + await iam.testResourceIamPermissions( + iamOrigin(), + "v1", + name, + ["iam.serviceAccounts.actAs"], + `projects/${projectId}`, + ); + } catch (err: unknown) { + if (!(err instanceof FirebaseError)) { + throw err; + } + if (err.status === 404) { + await provisionDefaultComputeServiceAccount(projectId); + } else if (err.status === 403) { + throw new FirebaseError( + `Failed to create backend due to missing delegation permissions for ${sa}. Make sure you have the iam.serviceAccounts.actAs permission.`, + { original: err }, + ); + } + } +} + +/** + * Prompts the user for a backend id and verifies that it doesn't match a pre-existing backend. + */ +async function promptNewBackendId( + projectId: string, + location: string, + prompt: any, +): Promise { + while (true) { + const backendId = await promptOnce(prompt); + try { + await apphosting.getBackend(projectId, location, backendId); + } catch (err: any) { + if (err.status === 404) { + return backendId; + } + throw new FirebaseError( + `Failed to check if backend with id ${backendId} already exists in ${location}`, + { original: err }, + ); + } + logWarning(`Backend with id ${backendId} already exists in ${location}`); + } +} + +function defaultComputeServiceAccountEmail(projectId: string): string { + return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`; +} + +/** + * Creates (and waits for) a new backend. Optionally may create the default compute service account if + * it was requested and doesn't exist. + */ +export async function createBackend( + projectId: string, + location: string, + backendId: string, + repository: GitRepositoryLink, + serviceAccount: string | null, + webAppId: string | undefined, + rootDir = "/", +): Promise { + const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId); + const backendReqBody: Omit = { + servingLocality: "GLOBAL_ACCESS", + codebase: { + repository: `${repository.name}`, + rootDirectory: rootDir, + }, + labels: deploymentTool.labels(), + serviceAccount: serviceAccount || defaultServiceAccount, + appId: webAppId, + }; + + async function createBackendAndPoll(): Promise { + const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId); + return await poller.pollOperation({ + ...apphostingPollerOptions, + pollerName: `create-${projectId}-${location}-${backendId}`, + operationResourceName: op.name, + }); + } + + return await createBackendAndPoll(); +} + +async function provisionDefaultComputeServiceAccount(projectId: string): Promise { + try { + await iam.createServiceAccount( + projectId, + DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, + "Default service account used to run builds and deploys for Firebase App Hosting", + "Firebase App Hosting compute service account", + ); + } catch (err: any) { + // 409 Already Exists errors can safely be ignored. + if (err.status !== 409) { + throw err; + } + } + await addServiceAccountToRoles( + projectId, + defaultComputeServiceAccountEmail(projectId), + [ + "roles/firebaseapphosting.computeRunner", + "roles/firebase.sdkAdminServiceAgent", + "roles/developerconnect.readTokenAccessor", + ], + /* skipAccountLookup= */ true, + ); +} + +/** + * Sets the default rollout policy to route 100% of traffic to the latest deploy. + */ +export async function setDefaultTrafficPolicy( + projectId: string, + location: string, + backendId: string, + codebaseBranch: string, +): Promise { + const traffic: DeepOmit = { + rolloutPolicy: { + codebaseBranch: codebaseBranch, + stages: [ + { + progression: "IMMEDIATE", + targetPercent: 100, + }, + ], + }, + }; + const op = await apphosting.updateTraffic(projectId, location, backendId, traffic); + await poller.pollOperation({ + ...apphostingPollerOptions, + pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, + operationResourceName: op.name, + }); +} + +/** + * Creates a new build and rollout and polls both to completion. + */ +export async function orchestrateRollout( + projectId: string, + location: string, + backendId: string, + buildInput: DeepOmit, +): Promise<{ rollout: Rollout; build: Build }> { + const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1); + const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput); + + const rolloutBody = { + build: `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`, + }; + + let tries = 0; + let done = false; + while (!done) { + tries++; + try { + const validateOnly = true; + await apphosting.createRollout( + projectId, + location, + backendId, + buildId, + rolloutBody, + validateOnly, + ); + done = true; + } catch (err: unknown) { + if (err instanceof FirebaseError && err.status === 400) { + if (tries >= 5) { + throw err; + } + await sleep(1000); + } else { + throw err; + } + } + } + + const rolloutOp = await apphosting.createRollout( + projectId, + location, + backendId, + buildId, + rolloutBody, + ); + + const rolloutPoll = poller.pollOperation({ + ...apphostingPollerOptions, + pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`, + operationResourceName: rolloutOp.name, + }); + const buildPoll = poller.pollOperation({ + ...apphostingPollerOptions, + pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`, + operationResourceName: buildOp.name, + }); + + const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]); + + if (build.state !== "READY") { + if (!build.buildLogsUri) { + throw new FirebaseError( + "Failed to build your app, but failed to get build logs as well. " + + "This is an internal error and should be reported", + ); + } + throw new FirebaseError( + `Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, + { children: [build.error] }, + ); + } + return { rollout, build }; +} + +/** + * Deletes the given backend. Polls till completion. + */ +export async function deleteBackendAndPoll( + projectId: string, + location: string, + backendId: string, +): Promise { + const op = await apphosting.deleteBackend(projectId, location, backendId); + await poller.pollOperation({ + ...apphostingPollerOptions, + pollerName: `delete-${projectId}-${location}-${backendId}`, + operationResourceName: op.name, + }); +} + +/** + * Prompts the user for a location. If there's only a single valid location, skips the prompt and returns that location. + */ +export async function promptLocation( + projectId: string, + prompt: string = "Please select a location:", +): Promise { + const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId); + if (allowedLocations.length === 1) { + return allowedLocations[0]; + } + + const location = (await promptOnce({ + name: "location", + type: "list", + default: DEFAULT_LOCATION, + message: prompt, + choices: allowedLocations, + })) as string; + + logSuccess(`Location set to ${location}.\n`); + + return location; +} + +/** + * Fetches a backend from the server. If there are multiple backends with that name (ie multi-regional backends), + * prompts the user to disambiguate. + */ +export async function getBackendForAmbiguousLocation( + projectId: string, + backendId: string, + locationDisambugationPrompt: string, +): Promise { + let { unreachable, backends } = await apphosting.listBackends(projectId, "-"); + if (unreachable && unreachable.length !== 0) { + logWarning( + `The following locations are currently unreachable: ${unreachable}.\n` + + "If your backend is in one of these regions, please try again later.", + ); + } + backends = backends.filter( + (backend) => apphosting.parseBackendName(backend.name).id === backendId, + ); + if (backends.length === 0) { + throw new FirebaseError(`No backend named "${backendId}" found.`); + } + if (backends.length === 1) { + return backends[0]; + } + + const backendsByLocation = new Map(); + backends.forEach((backend) => + backendsByLocation.set(apphosting.parseBackendName(backend.name).location, backend), + ); + const location = await promptOnce({ + name: "location", + type: "list", + message: locationDisambugationPrompt, + choices: [...backendsByLocation.keys()], + }); + return backendsByLocation.get(location)!; +} diff --git a/src/apphosting/repo.spec.ts b/src/apphosting/repo.spec.ts new file mode 100644 index 00000000000..fa4c5afad3e --- /dev/null +++ b/src/apphosting/repo.spec.ts @@ -0,0 +1,325 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as gcb from "../gcp/cloudbuild"; +import * as rm from "../gcp/resourceManager"; +import * as prompt from "../prompt"; +import * as poller from "../operation-poller"; +import * as repo from "./repo"; +import * as utils from "../utils"; +import * as srcUtils from "../getProjectNumber"; +import { FirebaseError } from "../error"; + +const projectId = "projectId"; +const location = "us-central1"; + +function mockConn(id: string): gcb.Connection { + return { + name: `projects/${projectId}/locations/${location}/connections/${id}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + }; +} + +function mockRepo(name: string): gcb.Repository { + return { + name: `${name}`, + remoteUri: `https://github.com/test/${name}.git`, + createTime: "", + updateTime: "", + }; +} + +function mockReposWithRandomUris(n: number): gcb.Repository[] { + const repos = []; + for (let i = 0; i < n; i++) { + const hash = Math.random().toString(36).slice(6); + repos.push(mockRepo(hash)); + } + return repos; +} + +describe("composer", () => { + describe("connect GitHub repo", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let promptOnceStub: sinon.SinonStub; + let pollOperationStub: sinon.SinonStub; + let getConnectionStub: sinon.SinonStub; + let getRepositoryStub: sinon.SinonStub; + let createConnectionStub: sinon.SinonStub; + let serviceAccountHasRolesStub: sinon.SinonStub; + let createRepositoryStub: sinon.SinonStub; + let fetchLinkableRepositoriesStub: sinon.SinonStub; + let getProjectNumberStub: sinon.SinonStub; + let openInBrowserPopupStub: sinon.SinonStub; + + beforeEach(() => { + promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call"); + pollOperationStub = sandbox + .stub(poller, "pollOperation") + .throws("Unexpected pollOperation call"); + getConnectionStub = sandbox + .stub(gcb, "getConnection") + .throws("Unexpected getConnection call"); + getRepositoryStub = sandbox + .stub(gcb, "getRepository") + .throws("Unexpected getRepository call"); + createConnectionStub = sandbox + .stub(gcb, "createConnection") + .throws("Unexpected createConnection call"); + serviceAccountHasRolesStub = sandbox.stub(rm, "serviceAccountHasRoles").resolves(true); + createRepositoryStub = sandbox + .stub(gcb, "createRepository") + .throws("Unexpected createRepository call"); + fetchLinkableRepositoriesStub = sandbox + .stub(gcb, "fetchLinkableRepositories") + .throws("Unexpected fetchLinkableRepositories call"); + sandbox.stub(utils, "openInBrowser").resolves(); + openInBrowserPopupStub = sandbox + .stub(utils, "openInBrowserPopup") + .throws("Unexpected openInBrowserPopup call"); + getProjectNumberStub = sandbox + .stub(srcUtils, "getProjectNumber") + .throws("Unexpected getProjectNumber call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + const projectId = "projectId"; + const location = "us-central1"; + const connectionId = `apphosting-${location}`; + + const op = { + name: `projects/${projectId}/locations/${location}/connections/${connectionId}`, + done: true, + }; + const pendingConn = { + name: `projects/${projectId}/locations/${location}/connections/${connectionId}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "PENDING_USER_OAUTH", + message: "pending", + actionUri: "https://google.com", + }, + reconciling: false, + }; + const completeConn = { + name: `projects/${projectId}/locations/${location}/connections/${connectionId}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + }; + const repos = { + repositories: [ + { + name: "repo0", + remoteUri: "https://github.com/test/repo0.git", + }, + { + name: "repo1", + remoteUri: "https://github.com/test/repo1.git", + }, + ], + }; + + it("creates a connection if it doesn't exist", async () => { + getConnectionStub.onFirstCall().rejects(new FirebaseError("error", { status: 404 })); + getConnectionStub.onSecondCall().resolves(completeConn); + createConnectionStub.resolves(op); + pollOperationStub.resolves(pendingConn); + promptOnceStub.onFirstCall().resolves("any key"); + + await repo.getOrCreateConnection(projectId, location, connectionId); + expect(createConnectionStub).to.be.calledWith(projectId, location, connectionId); + }); + + it("checks if secret manager admin role is granted for cloud build P4SA when creating an oauth connection", async () => { + getConnectionStub.onFirstCall().rejects(new FirebaseError("error", { status: 404 })); + getConnectionStub.onSecondCall().resolves(completeConn); + createConnectionStub.resolves(op); + pollOperationStub.resolves(pendingConn); + promptOnceStub.resolves("any key"); + getProjectNumberStub.onFirstCall().resolves(projectId); + openInBrowserPopupStub.resolves({ url: "", cleanup: sandbox.stub() }); + + await repo.getOrCreateOauthConnection(projectId, location); + expect(serviceAccountHasRolesStub).to.be.calledWith( + projectId, + `service-${projectId}@gcp-sa-cloudbuild.iam.gserviceaccount.com`, + ["roles/secretmanager.admin"], + true, + ); + }); + + it("creates repository if it doesn't exist", async () => { + getConnectionStub.resolves(completeConn); + fetchLinkableRepositoriesStub.resolves(repos); + promptOnceStub.onFirstCall().resolves(repos.repositories[0].remoteUri); + getRepositoryStub.rejects(new FirebaseError("error", { status: 404 })); + createRepositoryStub.resolves({ name: "op" }); + pollOperationStub.resolves(repos.repositories[0]); + + await repo.getOrCreateRepository( + projectId, + location, + connectionId, + repos.repositories[0].remoteUri, + ); + expect(createRepositoryStub).to.be.calledWith( + projectId, + location, + connectionId, + "test-repo0", + repos.repositories[0].remoteUri, + ); + }); + + it("re-uses existing repository it already exists", async () => { + getConnectionStub.resolves(completeConn); + fetchLinkableRepositoriesStub.resolves(repos); + promptOnceStub.onFirstCall().resolves(repos.repositories[0].remoteUri); + getRepositoryStub.resolves(repos.repositories[0]); + + const r = await repo.getOrCreateRepository( + projectId, + location, + connectionId, + repos.repositories[0].remoteUri, + ); + expect(r).to.be.deep.equal(repos.repositories[0]); + }); + }); + + describe("parseConnectionName", () => { + it("should parse valid connection name", () => { + const str = "projects/my-project/locations/us-central1/connections/my-conn"; + + const expected = { + projectId: "my-project", + location: "us-central1", + id: "my-conn", + }; + + expect(repo.parseConnectionName(str)).to.deep.equal(expected); + }); + + it("should return undefined for invalid", () => { + expect( + repo.parseConnectionName( + "projects/my-project/locations/us-central1/connections/my-conn/repositories/repo", + ), + ).to.be.undefined; + expect(repo.parseConnectionName("foobar")).to.be.undefined; + }); + }); + + describe("fetchAllRepositories", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let fetchLinkableRepositoriesStub: sinon.SinonStub; + + beforeEach(() => { + fetchLinkableRepositoriesStub = sandbox + .stub(gcb, "fetchLinkableRepositories") + .throws("Unexpected fetchLinkableRepositories call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("should fetch all repositories from multiple pages", async () => { + fetchLinkableRepositoriesStub.onFirstCall().resolves({ + repositories: mockReposWithRandomUris(10), + nextPageToken: "1234", + }); + fetchLinkableRepositoriesStub.onSecondCall().resolves({ + repositories: mockReposWithRandomUris(10), + }); + + const { repos, remoteUriToConnection } = await repo.fetchAllRepositories(projectId, [ + mockConn("conn0"), + ]); + + expect(repos.length).to.equal(20); + expect(Object.keys(remoteUriToConnection).length).to.equal(20); + }); + + it("should fetch all linkable repositories from multiple connections", async () => { + const conn0 = mockConn("conn0"); + const conn1 = mockConn("conn1"); + const repo0 = mockRepo("repo-0"); + const repo1 = mockRepo("repo-1"); + fetchLinkableRepositoriesStub.onFirstCall().resolves({ + repositories: [repo0], + }); + fetchLinkableRepositoriesStub.onSecondCall().resolves({ + repositories: [repo1], + }); + + const { repos, remoteUriToConnection } = await repo.fetchAllRepositories(projectId, [ + conn0, + conn1, + ]); + + expect(repos.length).to.equal(2); + expect(remoteUriToConnection).to.deep.equal({ + [repo0.remoteUri]: conn0, + [repo1.remoteUri]: conn1, + }); + }); + }); + + describe("listAppHostingConnections", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let listConnectionsStub: sinon.SinonStub; + + function extractId(name: string): string { + const parts = name.split("/"); + return parts.pop() ?? ""; + } + + beforeEach(() => { + listConnectionsStub = sandbox + .stub(gcb, "listConnections") + .throws("Unexpected getConnection call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("filters out non-apphosting connections", async () => { + listConnectionsStub.resolves([ + mockConn("apphosting-github-conn-baddcafe"), + mockConn("hooray-conn"), + mockConn("apphosting-github-conn-deadbeef"), + mockConn("apphosting-github-oauth"), + ]); + + const conns = await repo.listAppHostingConnections(projectId); + expect(conns).to.have.length(2); + expect(conns.map((c) => extractId(c.name))).to.include.members([ + "apphosting-github-conn-baddcafe", + "apphosting-github-conn-deadbeef", + ]); + }); + }); +}); diff --git a/src/apphosting/repo.ts b/src/apphosting/repo.ts new file mode 100644 index 00000000000..5fd913960e7 --- /dev/null +++ b/src/apphosting/repo.ts @@ -0,0 +1,397 @@ +import * as clc from "colorette"; + +import * as gcb from "../gcp/cloudbuild"; +import * as rm from "../gcp/resourceManager"; +import * as poller from "../operation-poller"; +import * as utils from "../utils"; +import { cloudbuildOrigin } from "../api"; +import { FirebaseError } from "../error"; +import { promptOnce } from "../prompt"; +import { getProjectNumber } from "../getProjectNumber"; + +import * as fuzzy from "fuzzy"; +import * as inquirer from "inquirer"; + +export interface ConnectionNameParts { + projectId: string; + location: string; + id: string; +} + +const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/; +const APPHOSTING_OAUTH_CONN_NAME = "apphosting-github-oauth"; +const CONNECTION_NAME_REGEX = + /^projects\/(?[^\/]+)\/locations\/(?[^\/]+)\/connections\/(?[^\/]+)$/; + +/** + * Exported for unit testing. + * + * Example: /projects/my-project/locations/us-central1/connections/my-connection-id => { + * projectId: "my-project", + * location: "us-central1", + * id: "my-connection-id", + * } + */ +export function parseConnectionName(name: string): ConnectionNameParts | undefined { + const match = CONNECTION_NAME_REGEX.exec(name); + + if (!match || typeof match.groups === undefined) { + return; + } + const { projectId, location, id } = match.groups as unknown as ConnectionNameParts; + return { + projectId, + location, + id, + }; +} + +const gcbPollerOptions: Omit = { + apiOrigin: cloudbuildOrigin(), + apiVersion: "v2", + masterTimeout: 25 * 60 * 1_000, + maxBackoff: 10_000, +}; + +/** + * Example usage: + * extractRepoSlugFromURI("https://github.com/user/repo.git") => "user/repo" + */ +function extractRepoSlugFromUri(remoteUri: string): string | undefined { + const match = /github.com\/(.+).git/.exec(remoteUri); + if (!match) { + return undefined; + } + return match[1]; +} + +/** + * Generates a repository ID. + * The relation is 1:* between Cloud Build Connection and GitHub Repositories. + */ +function generateRepositoryId(remoteUri: string): string | undefined { + return extractRepoSlugFromUri(remoteUri)?.replaceAll("/", "-"); +} + +/** + * Generates connection id that matches specific id format recognized by all Firebase clients. + */ +function generateConnectionId(): string { + const randomHash = Math.random().toString(36).slice(6); + return `apphosting-github-conn-${randomHash}`; +} + +const ADD_CONN_CHOICE = "@ADD_CONN"; + +/** + * Prompts the user to link their backend to a GitHub repository. + */ +export async function linkGitHubRepository( + projectId: string, + location: string, +): Promise { + utils.logBullet(clc.bold(`${clc.yellow("===")} Import a GitHub repository`)); + // Fetch the sentinel Oauth connection first which is needed to create further GitHub connections. + const oauthConn = await getOrCreateOauthConnection(projectId, location); + const existingConns = await listAppHostingConnections(projectId); + + if (existingConns.length === 0) { + existingConns.push( + await createFullyInstalledConnection(projectId, location, generateConnectionId(), oauthConn), + ); + } + + let repoRemoteUri: string | undefined; + let connection: gcb.Connection; + do { + if (repoRemoteUri === ADD_CONN_CHOICE) { + existingConns.push( + await createFullyInstalledConnection( + projectId, + location, + generateConnectionId(), + oauthConn, + ), + ); + } + + const selection = await promptRepositoryUri(projectId, existingConns); + repoRemoteUri = selection.remoteUri; + connection = selection.connection; + } while (repoRemoteUri === ADD_CONN_CHOICE); + + // Ensure that the selected connection exists in the same region as the backend + const { id: connectionId } = parseConnectionName(connection.name)!; + await getOrCreateConnection(projectId, location, connectionId, { + authorizerCredential: connection.githubConfig?.authorizerCredential, + appInstallationId: connection.githubConfig?.appInstallationId, + }); + + const repo = await getOrCreateRepository(projectId, location, connectionId, repoRemoteUri); + utils.logSuccess(`Successfully linked GitHub repository at remote URI`); + utils.logSuccess(`\t${repoRemoteUri}`); + return repo; +} + +/** + * Creates a new GCB GitHub connection resource and ensures that it is fully configured on the GitHub + * side (ie associated with an account/org and some subset of repos within that scope). + * Copies over Oauth creds from the sentinel Oauth connection to save the user from having to + * reauthenticate with GitHub. + */ +async function createFullyInstalledConnection( + projectId: string, + location: string, + connectionId: string, + oauthConn: gcb.Connection, +): Promise { + let conn = await createConnection(projectId, location, connectionId, { + authorizerCredential: oauthConn.githubConfig?.authorizerCredential, + }); + + while (conn.installationState.stage !== "COMPLETE") { + utils.logBullet("Install the Cloud Build GitHub app to enable access to GitHub repositories"); + const targetUri = conn.installationState.actionUri; + utils.logBullet(targetUri); + await utils.openInBrowser(targetUri); + await promptOnce({ + type: "input", + message: + "Press Enter once you have installed or configured the Cloud Build GitHub app to access your GitHub repo.", + }); + conn = await gcb.getConnection(projectId, location, connectionId); + } + + return conn; +} + +/** + * Gets or creates the sentinel GitHub connection resource that contains our Firebase-wide GitHub Oauth token. + * This Oauth token can be used to create other connections without reprompting the user to grant access. + */ +export async function getOrCreateOauthConnection( + projectId: string, + location: string, +): Promise { + let conn: gcb.Connection; + try { + conn = await gcb.getConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME); + } catch (err: unknown) { + if ((err as any).status === 404) { + // Cloud build P4SA requires the secret manager admin role. + // This is required when creating an initial connection which is the Oauth connection in our case. + await ensureSecretManagerAdminGrant(projectId); + conn = await createConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME); + } else { + throw err; + } + } + + while (conn.installationState.stage === "PENDING_USER_OAUTH") { + utils.logBullet("You must authorize the Cloud Build GitHub app."); + utils.logBullet("Sign in to GitHub and authorize Cloud Build GitHub app:"); + const { url, cleanup } = await utils.openInBrowserPopup( + conn.installationState.actionUri, + "Authorize the GitHub app", + ); + utils.logBullet(`\t${url}`); + await promptOnce({ + type: "input", + message: "Press Enter once you have authorized the app", + }); + cleanup(); + const { projectId, location, id } = parseConnectionName(conn.name)!; + conn = await gcb.getConnection(projectId, location, id); + } + return conn; +} + +async function promptRepositoryUri( + projectId: string, + connections: gcb.Connection[], +): Promise<{ remoteUri: string; connection: gcb.Connection }> { + const { repos, remoteUriToConnection } = await fetchAllRepositories(projectId, connections); + const remoteUri = await promptOnce({ + type: "autocomplete", + name: "remoteUri", + message: "Which GitHub repo do you want to deploy?", + source: (_: any, input = ""): Promise<(inquirer.DistinctChoice | inquirer.Separator)[]> => { + return new Promise((resolve) => + resolve([ + new inquirer.Separator(), + { + name: "Missing a repo? Select this option to configure your GitHub connection settings", + value: ADD_CONN_CHOICE, + }, + new inquirer.Separator(), + ...fuzzy + .filter(input, repos, { + extract: (repo) => extractRepoSlugFromUri(repo.remoteUri) || "", + }) + .map((result) => { + return { + name: extractRepoSlugFromUri(result.original.remoteUri) || "", + value: result.original.remoteUri, + }; + }), + ]), + ); + }, + }); + return { remoteUri, connection: remoteUriToConnection[remoteUri] }; +} + +async function ensureSecretManagerAdminGrant(projectId: string): Promise { + const projectNumber = await getProjectNumber({ projectId }); + const cbsaEmail = gcb.getDefaultServiceAgent(projectNumber); + + const alreadyGranted = await rm.serviceAccountHasRoles( + projectId, + cbsaEmail, + ["roles/secretmanager.admin"], + true, + ); + if (alreadyGranted) { + return; + } + + utils.logBullet( + "To create a new GitHub connection, Secret Manager Admin role (roles/secretmanager.admin) is required on the Cloud Build Service Agent.", + ); + const grant = await promptOnce({ + type: "confirm", + message: "Grant the required role to the Cloud Build Service Agent?", + }); + if (!grant) { + utils.logBullet( + "You, or your project administrator, should run the following command to grant the required role:\n\n" + + "You, or your project adminstrator, can run the following command to grant the required role manually:\n\n" + + `\tgcloud projects add-iam-policy-binding ${projectId} \\\n` + + `\t --member="serviceAccount:${cbsaEmail} \\\n` + + `\t --role="roles/secretmanager.admin\n`, + ); + throw new FirebaseError("Insufficient IAM permissions to create a new connection to GitHub"); + } + await rm.addServiceAccountToRoles(projectId, cbsaEmail, ["roles/secretmanager.admin"], true); + utils.logSuccess("Successfully granted the required role to the Cloud Build Service Agent!"); +} + +/** + * Creates a new Cloud Build Connection resource. Will typically need some initialization + * or configuration after being created. + */ +export async function createConnection( + projectId: string, + location: string, + connectionId: string, + githubConfig?: gcb.GitHubConfig, +): Promise { + const op = await gcb.createConnection(projectId, location, connectionId, githubConfig); + const conn = await poller.pollOperation({ + ...gcbPollerOptions, + pollerName: `create-${location}-${connectionId}`, + operationResourceName: op.name, + }); + return conn; +} + +/** + * Exported for unit testing. + */ +export async function getOrCreateConnection( + projectId: string, + location: string, + connectionId: string, + githubConfig?: gcb.GitHubConfig, +): Promise { + let conn: gcb.Connection; + try { + conn = await gcb.getConnection(projectId, location, connectionId); + } catch (err: unknown) { + if ((err as any).status === 404) { + conn = await createConnection(projectId, location, connectionId, githubConfig); + } else { + throw err; + } + } + return conn; +} + +/** + * Exported for unit testing. + */ +export async function getOrCreateRepository( + projectId: string, + location: string, + connectionId: string, + remoteUri: string, +): Promise { + const repositoryId = generateRepositoryId(remoteUri); + if (!repositoryId) { + throw new FirebaseError(`Failed to generate repositoryId for URI "${remoteUri}".`); + } + let repo: gcb.Repository; + try { + repo = await gcb.getRepository(projectId, location, connectionId, repositoryId); + } catch (err: unknown) { + if ((err as FirebaseError).status === 404) { + const op = await gcb.createRepository( + projectId, + location, + connectionId, + repositoryId, + remoteUri, + ); + repo = await poller.pollOperation({ + ...gcbPollerOptions, + pollerName: `create-${location}-${connectionId}-${repositoryId}`, + operationResourceName: op.name, + }); + } else { + throw err; + } + } + return repo; +} + +/** + * Exported for unit testing. + */ +export async function listAppHostingConnections(projectId: string) { + const conns = await gcb.listConnections(projectId, "-"); + return conns.filter( + (conn) => + APPHOSTING_CONN_PATTERN.test(conn.name) && + conn.installationState.stage === "COMPLETE" && + !conn.disabled, + ); +} + +/** + * Exported for unit testing. + */ +export async function fetchAllRepositories( + projectId: string, + connections: gcb.Connection[], +): Promise<{ repos: gcb.Repository[]; remoteUriToConnection: Record }> { + const repos: gcb.Repository[] = []; + const remoteUriToConnection: Record = {}; + + const getNextPage = async (conn: gcb.Connection, pageToken = ""): Promise => { + const { location, id } = parseConnectionName(conn.name)!; + const resp = await gcb.fetchLinkableRepositories(projectId, location, id, pageToken); + if (resp.repositories && resp.repositories.length > 0) { + for (const repo of resp.repositories) { + repos.push(repo); + remoteUriToConnection[repo.remoteUri] = conn; + } + } + if (resp.nextPageToken) { + await getNextPage(conn, resp.nextPageToken); + } + }; + for (const conn of connections) { + await getNextPage(conn); + } + return { repos, remoteUriToConnection }; +} diff --git a/src/apphosting/secrets/dialogs.spec.ts b/src/apphosting/secrets/dialogs.spec.ts new file mode 100644 index 00000000000..d3104965014 --- /dev/null +++ b/src/apphosting/secrets/dialogs.spec.ts @@ -0,0 +1,469 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as clc from "colorette"; + +import * as secrets from "."; +import * as dialogs from "./dialogs"; +import * as apphosting from "../../gcp/apphosting"; +import * as utilsImport from "../../utils"; +import * as promptImport from "../../prompt"; + +describe("dialogs", () => { + const modernA = { + name: "projects/p/locations/l/backends/modernA", + serviceAccount: "a", + } as any as apphosting.Backend; + const modernA2 = { + name: "projects/p/locations/l2/backends/modernA2", + serviceAccount: "a", + } as any as apphosting.Backend; + const modernB = { + name: "projects/p/locations/l/backends/modernB", + serviceAccount: "b", + } as any as apphosting.Backend; + const legacy = { + name: "projects/p/locations/l/backends/legacy", + } as any as apphosting.Backend; + const legacy2 = { + name: "projects/p/locations/l/backends/legacy2", + } as any as apphosting.Backend; + + const emptyMulti: secrets.MultiServiceAccounts = { + buildServiceAccounts: [], + runServiceAccounts: [], + }; + + describe("toMetadata", () => { + it("handles explicit account", () => { + // Note: passing in out of order to verify the results are sorted. + const metadata = dialogs.toMetadata("number", [modernA2, modernA]); + + expect(metadata).to.deep.equal([ + { location: "l", id: "modernA", buildServiceAccount: "a", runServiceAccount: "a" }, + { location: "l2", id: "modernA2", buildServiceAccount: "a", runServiceAccount: "a" }, + ]); + }); + + it("handles fallback for legacy SAs", () => { + const metadata = dialogs.toMetadata("number", [modernA, legacy]); + + expect(metadata).to.deep.equal([ + { + location: "l", + id: "legacy", + ...secrets.serviceAccountsForBackend("number", legacy), + }, + { location: "l", id: "modernA", buildServiceAccount: "a", runServiceAccount: "a" }, + ]); + }); + + it("sorts by location first and id second", () => { + const metadata = dialogs.toMetadata("number", [legacy, modernA, modernA2]); + expect(metadata).to.deep.equal([ + { + location: "l", + id: "legacy", + ...secrets.serviceAccountsForBackend("number", legacy), + }, + { location: "l", id: "modernA", buildServiceAccount: "a", runServiceAccount: "a" }, + { location: "l2", id: "modernA2", buildServiceAccount: "a", runServiceAccount: "a" }, + ]); + }); + }); + + it("serviceAccountDisplay", () => { + expect( + dialogs.serviceAccountDisplay({ buildServiceAccount: "build", runServiceAccount: "run" }), + ).to.equal("build, run"); + expect( + dialogs.serviceAccountDisplay({ buildServiceAccount: "common", runServiceAccount: "common" }), + ).to.equal("common"); + }); + + describe("tableForBackends", () => { + it("uses 'service account' header if all backends use one service account", () => { + const table = dialogs.tableForBackends(dialogs.toMetadata("number", [modernA, modernB])); + expect(table[0]).to.deep.equal(["location", "backend", "service account"]); + expect(table[1]).to.deep.equal([ + ["l", "modernA", "a"], + ["l", "modernB", "b"], + ]); + }); + + it("uses 'service accounts' header if any backend uses more than one service accont", () => { + const table = dialogs.tableForBackends(dialogs.toMetadata("number", [legacy, modernA])); + const legacyAccounts = secrets.serviceAccountsForBackend("number", legacy); + expect(table[0]).to.deep.equal(["location", "backend", "service accounts"]); + expect(table[1]).to.deep.equal([ + [ + "l", + "legacy", + `${legacyAccounts.buildServiceAccount}, ${legacyAccounts.runServiceAccount}`, + ], + ["l", "modernA", "a"], + ]); + }); + }); + + it("selectFromMetadata", () => { + const metadata: secrets.ServiceAccounts[] = [ + { + buildServiceAccount: "build", + runServiceAccount: "run", + }, + { + buildServiceAccount: "common", + runServiceAccount: "common", + }, + { + buildServiceAccount: "omittedBuild", + runServiceAccount: "omittedRun", + }, + ]; + expect(dialogs.selectFromMetadata(metadata, ["build", "run", "common"])).to.deep.equal({ + buildServiceAccounts: ["build", "common"], + runServiceAccounts: ["run"], + }); + }); + + describe("selectBackendServiceAccounts", () => { + let listBackends: sinon.SinonStub; + let utils: sinon.SinonStubbedInstance; + let prompt: sinon.SinonStubbedInstance; + + beforeEach(() => { + listBackends = sinon.stub(apphosting, "listBackends"); + utils = sinon.stub(utilsImport); + prompt = sinon.stub(promptImport); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("handles no backends", async () => { + listBackends.resolves({ + backends: [], + unreachable: [], + }); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(emptyMulti); + expect(utils.logWarning).to.have.been.calledWith(dialogs.WARN_NO_BACKENDS); + }); + + it("handles unreachable regions", async () => { + listBackends.resolves({ + backends: [], + unreachable: ["us-central1"], + }); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(emptyMulti); + + expect(utils.logWarning).to.have.been.calledWith( + `Could not reach location(s) us-central1. You may need to run ${clc.bold("firebase apphosting:secrets:grantaccess")} ` + + "at a later time if you have backends in these locations", + ); + expect(utils.logWarning).to.have.been.calledWith(dialogs.WARN_NO_BACKENDS); + }); + + it("handles a single backend (opt yes)", async () => { + listBackends.resolves({ + backends: [modernA], + unreachable: [], + }); + prompt.confirm.resolves(true); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal({ + buildServiceAccounts: [modernA.serviceAccount], + runServiceAccounts: [], + }); + + expect(prompt.confirm).to.have.been.calledWith({ + nonInteractive: undefined, + default: true, + message: + "To use this secret, your backend's service account must be granted access. Would you like to grant access now?", + }); + expect(utils.logBullet).to.not.have.been.called; + }); + + it("handles a single backend (opt no)", async () => { + listBackends.resolves({ + backends: [modernA], + unreachable: [], + }); + prompt.confirm.resolves(false); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(emptyMulti); + + expect(prompt.confirm).to.have.been.calledWith({ + nonInteractive: undefined, + default: true, + message: + "To use this secret, your backend's service account must be granted access. Would you like to grant access now?", + }); + expect(utils.logBullet).to.have.been.calledWith(dialogs.GRANT_ACCESS_IN_FUTURE); + }); + + it("handles multiple backends with the same (multiple) SAs (opt yes)", async () => { + listBackends.resolves({ + backends: [legacy, legacy2], + unreachable: [], + }); + prompt.confirm.resolves(true); + const accounts = secrets.serviceAccountsForBackend("number", legacy); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(secrets.toMulti(accounts)); + + expect(utils.logBullet.getCall(0).args[0]).to.eq( + "To use this secret, your backend's service account must be granted access.", + ); + + expect(utils.logBullet.getCall(1).args[0]).to.eq( + `All of your backends share the following service accounts: ${dialogs.serviceAccountDisplay(accounts)}.` + + "\nGranting access to one backend will grant access to all backends.", + ); + + expect(prompt.confirm).to.have.been.calledWith({ + nonInteractive: undefined, + default: true, + message: "Would you like to grant access to all backends now?", + }); + expect(utils.logBullet).to.have.been.calledTwice; + }); + + it("handles multiple backends with the same (multiple) SAs (opt no)", async () => { + listBackends.resolves({ + backends: [legacy, legacy2], + unreachable: [], + }); + prompt.confirm.resolves(false); + const legacyAccounts = secrets.serviceAccountsForBackend("number", legacy); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(emptyMulti); + + expect(utils.logBullet.getCall(0).args[0]).to.eq( + "To use this secret, your backend's service account must be granted access.", + ); + + expect(utils.logBullet.getCall(1).args[0]).to.eq( + `All of your backends share the following service accounts: ${dialogs.serviceAccountDisplay(legacyAccounts)}.` + + "\nGranting access to one backend will grant access to all backends.", + ); + + expect(prompt.confirm).to.have.been.calledWith({ + nonInteractive: undefined, + default: true, + message: "Would you like to grant access to all backends now?", + }); + expect(utils.logBullet).to.have.been.calledWith(dialogs.GRANT_ACCESS_IN_FUTURE); + }); + + it("handles multiple backends with the same (single) SA (opt yes)", async () => { + listBackends.resolves({ + backends: [modernA, modernA2], + unreachable: [], + }); + prompt.confirm.resolves(true); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal({ + buildServiceAccounts: [modernA.serviceAccount], + runServiceAccounts: [], + }); + + expect(utils.logBullet.getCall(0).args[0]).to.eq( + "To use this secret, your backend's service account must be granted access.", + ); + + expect(utils.logBullet.getCall(1).args[0]).to.eq( + `All of your backends share the following service account: a.` + + "\nGranting access to one backend will grant access to all backends.", + ); + + expect(prompt.confirm).to.have.been.calledWith({ + nonInteractive: undefined, + default: true, + message: "Would you like to grant access to all backends now?", + }); + + expect(utils.logBullet).to.have.been.calledTwice; + }); + + it("handles multiple backends with the same (single) SA (opt no)", async () => { + listBackends.resolves({ + backends: [modernA, modernA2], + unreachable: [], + }); + prompt.confirm.resolves(false); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(emptyMulti); + + expect(utils.logBullet.getCall(0).args[0]).to.eq( + "To use this secret, your backend's service account must be granted access.", + ); + + expect(utils.logBullet.getCall(1).args[0]).to.eq( + `All of your backends share the following service account: a.` + + "\nGranting access to one backend will grant access to all backends.", + ); + + expect(prompt.confirm).to.have.been.calledWith({ + nonInteractive: undefined, + default: true, + message: "Would you like to grant access to all backends now?", + }); + expect(utils.logBullet).to.have.been.calledWith(dialogs.GRANT_ACCESS_IN_FUTURE); + }); + + it("handles multiple backends with different SAs (select some)", async () => { + listBackends.resolves({ + backends: [modernA, modernA2, modernB, legacy, legacy2], + unreachable: [], + }); + prompt.promptOnce.resolves(["a", "b"]); + const legacyAccounts = secrets.serviceAccountsForBackend("number", legacy); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal({ buildServiceAccounts: ["a", "b"], runServiceAccounts: [] }); + + expect(prompt.promptOnce).to.have.been.calledWith({ + type: "checkbox", + message: + "Which service accounts would you like to grant access? Press Space to select accounts, then Enter to confirm your choices.", + choices: [ + "a", + "b", + legacyAccounts.buildServiceAccount, + legacyAccounts.runServiceAccount, + ].sort(), + }); + expect(utils.logBullet).to.have.been.calledWith( + "To use this secret, your backend's service account must be granted access. Your backends use the following service accounts:", + ); + expect(utils.logBullet).to.not.have.been.calledWith(dialogs.GRANT_ACCESS_IN_FUTURE); + }); + + it("handles multiple backends with different SAs (select none)", async () => { + listBackends.resolves({ + backends: [modernA, modernA2, modernB, legacy, legacy2], + unreachable: [], + }); + prompt.promptOnce.resolves([]); + const legacyAccounts = secrets.serviceAccountsForBackend("number", legacy); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(emptyMulti); + + expect(prompt.promptOnce).to.have.been.calledWith({ + type: "checkbox", + message: + "Which service accounts would you like to grant access? Press Space to select accounts, then Enter to confirm your choices.", + choices: [ + "a", + "b", + legacyAccounts.buildServiceAccount, + legacyAccounts.runServiceAccount, + ].sort(), + }); + expect(utils.logBullet).to.have.been.calledWith( + "To use this secret, your backend's service account must be granted access. Your backends use the following service accounts:", + ); + expect(utils.logBullet).to.have.been.calledWith(dialogs.GRANT_ACCESS_IN_FUTURE); + }); + }); + + describe("envVarForSecret", () => { + let prompt: sinon.SinonStubbedInstance; + let utils: sinon.SinonStubbedInstance; + + beforeEach(() => { + prompt = sinon.stub(promptImport); + utils = sinon.stub(utilsImport); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("accepts a valid env var", async () => { + await expect(dialogs.envVarForSecret("VALID_KEY")).to.eventually.equal("VALID_KEY"); + expect(prompt.promptOnce).to.not.have.been.called; + }); + + it("suggests a valid upper case name", async () => { + prompt.promptOnce.resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.promptOnce).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + }); + + it("prevents invalid keys", async () => { + prompt.promptOnce.onFirstCall().resolves("secret-value"); + prompt.promptOnce.onSecondCall().resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.promptOnce).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + expect(prompt.promptOnce).to.have.been.calledTwice; + expect(utils.logLabeledError).to.have.been.calledWith( + "apphosting", + "Key secret-value must start with an uppercase ASCII letter or underscore, and then consist of uppercase ASCII letters, digits, and underscores.", + ); + }); + + it("prevents reserved keys", async () => { + prompt.promptOnce.onFirstCall().resolves("PORT"); + prompt.promptOnce.onSecondCall().resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.promptOnce).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + expect(prompt.promptOnce).to.have.been.calledTwice; + expect(utils.logLabeledError).to.have.been.calledWith( + "apphosting", + "Key PORT is reserved for internal use.", + ); + }); + + it("prevents reserved prefixes", async () => { + prompt.promptOnce.onFirstCall().resolves("X_GOOGLE_SECRET"); + prompt.promptOnce.onSecondCall().resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.promptOnce).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + expect(prompt.promptOnce).to.have.been.calledTwice; + expect(utils.logLabeledError).to.have.been.calledWithMatch( + "apphosting", + /Key X_GOOGLE_SECRET starts with a reserved prefix/, + ); + }); + }); +}); diff --git a/src/apphosting/secrets/dialogs.ts b/src/apphosting/secrets/dialogs.ts new file mode 100644 index 00000000000..d3fd2cccd03 --- /dev/null +++ b/src/apphosting/secrets/dialogs.ts @@ -0,0 +1,234 @@ +import * as clc from "colorette"; +const Table = require("cli-table"); + +import { MultiServiceAccounts, ServiceAccounts, serviceAccountsForBackend, toMulti } from "."; +import * as apphosting from "../../gcp/apphosting"; +import * as prompt from "../../prompt"; +import * as utils from "../../utils"; +import { logger } from "../../logger"; + +// TODO: Consider moving some of this into a common utility +import * as env from "../../functions/env"; + +interface BackendMetadata { + location: string; + id: string; + buildServiceAccount: string; + runServiceAccount: string; +} + +/** + * Creates sorted BackendMetadata for a list of Backends. + */ +export function toMetadata( + projectNumber: string, + backends: apphosting.Backend[], +): BackendMetadata[] { + const metadata: BackendMetadata[] = []; + for (const backend of backends) { + // Splits format projects//locations//backends/ + const [, , , location, , id] = backend.name.split("/"); + metadata.push({ location, id, ...serviceAccountsForBackend(projectNumber, backend) }); + } + return metadata.sort((left, right) => { + const cmplocation = left.location.localeCompare(right.location); + if (cmplocation) { + return cmplocation; + } + return left.id.localeCompare(right.id); + }); +} + +/** Displays a single service account or a comma separated list of service accounts. */ +export function serviceAccountDisplay(metadata: ServiceAccounts): string { + if (sameServiceAccount(metadata)) { + return metadata.runServiceAccount; + } + return `${metadata.buildServiceAccount}, ${metadata.runServiceAccount}`; +} + +function sameServiceAccount(metadata: ServiceAccounts): boolean { + return metadata.buildServiceAccount === metadata.runServiceAccount; +} + +const matchesServiceAccounts = (target: ServiceAccounts) => (test: ServiceAccounts) => { + return ( + target.buildServiceAccount === test.buildServiceAccount && + target.runServiceAccount === test.runServiceAccount + ); +}; + +/** + * Given a list of BackendMetadata, creates the JSON necessary to power a cli table. + * @returns a tuple where the first element is column names and the second element is rows. + */ +export function tableForBackends( + metadata: BackendMetadata[], +): [headers: string[], rows: string[][]] { + const headers = [ + "location", + "backend", + metadata.every(sameServiceAccount) ? "service account" : "service accounts", + ]; + const rows = metadata.map((m) => [m.location, m.id, serviceAccountDisplay(m)]); + return [headers, rows]; +} + +/** + * Returns a MultiServiceAccounts for all selected service accounts in a ServiceAccount[]. + * If a service account is ever a "build" account in input, it will be a "build" account in the + * output. Otherwise, it will be a "run" account. + */ +export function selectFromMetadata( + input: ServiceAccounts[], + selected: string[], +): MultiServiceAccounts { + const buildAccounts = new Set(); + const runAccounts = new Set(); + + for (const sa of selected) { + if (input.find((m) => m.buildServiceAccount === sa)) { + buildAccounts.add(sa); + } else { + runAccounts.add(sa); + } + } + + return { + buildServiceAccounts: [...buildAccounts], + runServiceAccounts: [...runAccounts], + }; +} + +/** Common warning log that there are no backends. Exported to make tests easier. */ +export const WARN_NO_BACKENDS = + "To use this secret, your backend's service account must be granted access." + + "It does not look like you have a backend yet. After creating a backend, grant access with " + + clc.bold("firebase apphosting:secrets:grantaccess"); + +/** Common warning log that the user will need to grant access manually. Exported to make tests easier. */ +export const GRANT_ACCESS_IN_FUTURE = `To grant access in the future, run ${clc.bold("firebase apphosting:secrets:grantaccess")}`; + +/** + * Create a dialog where customers can choose a series of service accounts to grant access. + * Can return an empty array of the user opts out of granting access. + */ +export async function selectBackendServiceAccounts( + projectNumber: string, + projectId: string, + options: any, +): Promise { + const listBackends = await apphosting.listBackends(projectId, "-"); + + if (listBackends.unreachable.length) { + utils.logWarning( + `Could not reach location(s) ${listBackends.unreachable.join(", ")}. You may need to run ` + + `${clc.bold("firebase apphosting:secrets:grantaccess")} at a later time if you have backends in these locations`, + ); + } + + if (!listBackends.backends.length) { + utils.logWarning(WARN_NO_BACKENDS); + return { buildServiceAccounts: [], runServiceAccounts: [] }; + } + + if (listBackends.backends.length === 1) { + const grant = await prompt.confirm({ + nonInteractive: options.nonInteractive, + default: true, + message: + "To use this secret, your backend's service account must be granted access. Would you like to grant access now?", + }); + if (grant) { + return toMulti(serviceAccountsForBackend(projectNumber, listBackends.backends[0])); + } + utils.logBullet(GRANT_ACCESS_IN_FUTURE); + return { buildServiceAccounts: [], runServiceAccounts: [] }; + } + + const metadata: BackendMetadata[] = toMetadata(projectNumber, listBackends.backends); + + if (metadata.every(matchesServiceAccounts(metadata[0]))) { + utils.logBullet("To use this secret, your backend's service account must be granted access."); + utils.logBullet( + "All of your backends share the following " + + (sameServiceAccount(metadata[0]) ? "service account: " : "service accounts: ") + + serviceAccountDisplay(metadata[0]) + + ".\nGranting access to one backend will grant access to all backends.", + ); + const grant = await prompt.confirm({ + nonInteractive: options.nonInteractive, + default: true, + message: "Would you like to grant access to all backends now?", + }); + if (grant) { + return selectFromMetadata(metadata, [ + metadata[0].buildServiceAccount, + metadata[0].runServiceAccount, + ]); + } + utils.logBullet(GRANT_ACCESS_IN_FUTURE); + return { buildServiceAccounts: [], runServiceAccounts: [] }; + } + + utils.logBullet( + "To use this secret, your backend's service account must be granted access. Your backends use the following service accounts:", + ); + const tableData = tableForBackends(metadata); + const table = new Table({ + head: tableData[0], + style: { head: ["green"] }, + rows: tableData[1], + }); + logger.info(table.toString()); + + const allAccounts = metadata.reduce((accum: Set, row) => { + accum.add(row.buildServiceAccount); + accum.add(row.runServiceAccount); + return accum; + }, new Set()); + const chosen = await prompt.promptOnce({ + type: "checkbox", + message: + "Which service accounts would you like to grant access? " + + "Press Space to select accounts, then Enter to confirm your choices.", + choices: [...allAccounts.values()].sort(), + }); + if (!chosen.length) { + utils.logBullet(GRANT_ACCESS_IN_FUTURE); + } + return selectFromMetadata(metadata, chosen); +} + +function toUpperSnakeCase(key: string): string { + return key + .replace(/[.-]/g, "_") + .replace(/([a-z])([A-Z])/g, "$1_$2") + .toUpperCase(); +} + +export async function envVarForSecret(secret: string): Promise { + const upper = toUpperSnakeCase(secret); + if (upper === secret) { + try { + env.validateKey(secret); + return secret; + } catch { + // fallthrough + } + } + + do { + const test = await prompt.promptOnce({ + message: "What environment variable name would you like to use?", + default: upper, + }); + + try { + env.validateKey(test); + return test; + } catch (err) { + utils.logLabeledError("apphosting", (err as env.KeyValidationError).message); + } + } while (true); +} diff --git a/src/apphosting/secrets/index.spec.ts b/src/apphosting/secrets/index.spec.ts new file mode 100644 index 00000000000..dd2e00f6a1d --- /dev/null +++ b/src/apphosting/secrets/index.spec.ts @@ -0,0 +1,259 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as apphosting from "../../gcp/apphosting"; +import * as secrets from "."; +import * as iam from "../../gcp/iam"; +import * as gcb from "../../gcp/cloudbuild"; +import * as gce from "../../gcp/computeEngine"; +import * as gcsmImport from "../../gcp/secretManager"; +import * as utilsImport from "../../utils"; +import * as promptImport from "../../prompt"; + +describe("secrets", () => { + let gcsm: sinon.SinonStubbedInstance; + let utils: sinon.SinonStubbedInstance; + let prompt: sinon.SinonStubbedInstance; + + beforeEach(() => { + gcsm = sinon.stub(gcsmImport); + utils = sinon.stub(utilsImport); + prompt = sinon.stub(promptImport); + gcsm.isFunctionsManaged.restore(); + gcsm.labels.restore(); + gcsm.getIamPolicy.throws("Unexpected getIamPolicy call"); + gcsm.setIamPolicy.throws("Unexpected setIamPolicy call"); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + describe("serviceAccountsForbackend", () => { + it("uses explicit account", () => { + const backend = { + serviceAccount: "sa", + } as any as apphosting.Backend; + expect(secrets.serviceAccountsForBackend("number", backend)).to.deep.equal({ + buildServiceAccount: "sa", + runServiceAccount: "sa", + }); + }); + + it("has a fallback for legacy SAs", () => { + const backend = {} as any as apphosting.Backend; + expect(secrets.serviceAccountsForBackend("number", backend)).to.deep.equal({ + buildServiceAccount: gcb.getDefaultServiceAccount("number"), + runServiceAccount: gce.getDefaultServiceAccount("number"), + }); + }); + }); + + describe("upsertSecret", () => { + it("errors if a user tries to change replication policies (was global)", async () => { + gcsm.getSecret.withArgs("project", "secret").resolves({ + name: "secret", + projectId: "project", + labels: gcsm.labels("apphosting"), + replication: { + automatic: {}, + }, + }); + await expect(secrets.upsertSecret("project", "secret", "us-central1")).to.eventually.equal( + null, + ); + expect(utils.logLabeledError).to.have.been.calledWith( + "apphosting", + "Secret replication policies cannot be changed after creation", + ); + }); + + it("errors if a user tries to change replication policies (was another region)", async () => { + gcsm.getSecret.withArgs("project", "secret").resolves({ + name: "secret", + projectId: "project", + labels: gcsm.labels("apphosting"), + replication: { + userManaged: { + replicas: [ + { + location: "us-west1", + }, + ], + }, + }, + }); + await expect(secrets.upsertSecret("project", "secret", "us-central1")).to.eventually.equal( + null, + ); + expect(utils.logLabeledError).to.have.been.calledWith( + "apphosting", + "Secret replication policies cannot be changed after creation", + ); + }); + + it("noops if a secret already exists (location set)", async () => { + gcsm.getSecret.withArgs("project", "secret").resolves({ + name: "secret", + projectId: "project", + labels: gcsm.labels("apphosting"), + replication: { + userManaged: { + replicas: [ + { + location: "us-central1", + }, + ], + }, + }, + }); + await expect(secrets.upsertSecret("project", "secret", "us-central1")).to.eventually.equal( + false, + ); + expect(utils.logLabeledError).to.not.have.been.called; + }); + + it("noops if a secret already exists (automatic replication)", async () => { + gcsm.getSecret.withArgs("project", "secret").resolves({ + name: "secret", + projectId: "project", + labels: gcsm.labels("apphosting"), + replication: { + automatic: {}, + }, + }); + await expect(secrets.upsertSecret("project", "secret")).to.eventually.equal(false); + expect(utils.logLabeledError).to.not.have.been.called; + }); + + it("confirms before erasing functions garbage collection (choose yes)", async () => { + gcsm.getSecret.withArgs("project", "secret").resolves({ + name: "secret", + projectId: "project", + labels: gcsm.labels("functions"), + replication: { + automatic: {}, + }, + }); + prompt.confirm.resolves(true); + await expect(secrets.upsertSecret("project", "secret")).to.eventually.equal(false); + expect(utils.logLabeledWarning).to.have.been.calledWith( + "apphosting", + "Cloud Functions for Firebase currently manages versions of secret. " + + "Continuing will disable automatic deletion of old versions.", + ); + expect(prompt.confirm).to.have.been.calledWithMatch({ + message: "Do you wish to continue?", + default: false, + }); + expect(gcsm.patchSecret).to.have.been.calledWithMatch("project", "secret", {}); + }); + + it("confirms before erasing functions garbage collection (choose no)", async () => { + gcsm.getSecret.withArgs("project", "secret").resolves({ + name: "secret", + projectId: "project", + labels: gcsm.labels("functions"), + replication: { + automatic: {}, + }, + }); + prompt.confirm.resolves(false); + await expect(secrets.upsertSecret("project", "secret")).to.eventually.equal(null); + expect(utils.logLabeledWarning).to.have.been.calledWith( + "apphosting", + "Cloud Functions for Firebase currently manages versions of secret. " + + "Continuing will disable automatic deletion of old versions.", + ); + expect(prompt.confirm).to.have.been.calledWithMatch({ + message: "Do you wish to continue?", + default: false, + }); + expect(gcsm.patchSecret).to.not.have.been.called; + }); + + it("Creates a secret if none exists", async () => { + gcsm.getSecret.withArgs("project", "secret").rejects({ status: 404 }); + + await expect(secrets.upsertSecret("project", "secret")).to.eventually.equal(true); + + expect(gcsm.createSecret).to.have.been.calledWithMatch( + "project", + "secret", + gcsm.labels("apphosting"), + undefined, + ); + }); + }); + + describe("toMulti", () => { + it("handles different service accounts", () => { + expect( + secrets.toMulti({ buildServiceAccount: "buildSA", runServiceAccount: "computeSA" }), + ).to.deep.equal({ + buildServiceAccounts: ["buildSA"], + runServiceAccounts: ["computeSA"], + }); + }); + + it("handles the same service account", () => { + expect( + secrets.toMulti({ buildServiceAccount: "explicitSA", runServiceAccount: "explicitSA" }), + ).to.deep.equal({ + buildServiceAccounts: ["explicitSA"], + runServiceAccounts: [], + }); + }); + }); + + describe("grantSecretAccess", () => { + const secret = { + name: "secret", + projectId: "projectId", + }; + const existingPolicy: iam.Policy = { + version: 1, + etag: "tag", + bindings: [ + { + role: "roles/viewer", + members: ["serviceAccount:existingSA"], + }, + ], + }; + + it("should grant access to the appropriate service accounts", async () => { + gcsm.getIamPolicy.resolves(existingPolicy); + gcsm.setIamPolicy.resolves(); + + await secrets.grantSecretAccess(secret.projectId, "12345", secret.name, { + buildServiceAccounts: ["buildSA"], + runServiceAccounts: ["computeSA"], + }); + + const newBindings: iam.Binding[] = [ + { + role: "roles/viewer", + members: [`serviceAccount:existingSA`], + }, + { + role: "roles/secretmanager.secretAccessor", + members: ["serviceAccount:buildSA", "serviceAccount:computeSA"], + }, + { + role: "roles/secretmanager.viewer", + members: ["serviceAccount:buildSA"], + }, + { + role: "roles/secretmanager.secretVersionManager", + members: [ + "serviceAccount:service-12345@gcp-sa-firebaseapphosting.iam.gserviceaccount.com", + ], + }, + ]; + + expect(gcsm.getIamPolicy).to.be.calledWithMatch(secret); + expect(gcsm.setIamPolicy).to.be.calledWithMatch(secret, newBindings); + }); + }); +}); diff --git a/src/apphosting/secrets/index.ts b/src/apphosting/secrets/index.ts new file mode 100644 index 00000000000..8fd96e5cf0c --- /dev/null +++ b/src/apphosting/secrets/index.ts @@ -0,0 +1,166 @@ +import { FirebaseError } from "../../error"; +import * as iam from "../../gcp/iam"; +import * as gcsm from "../../gcp/secretManager"; +import * as gcb from "../../gcp/cloudbuild"; +import * as gce from "../../gcp/computeEngine"; +import * as apphosting from "../../gcp/apphosting"; +import { FIREBASE_MANAGED } from "../../gcp/secretManager"; +import { isFunctionsManaged } from "../../gcp/secretManager"; +import * as utils from "../../utils"; +import * as prompt from "../../prompt"; + +/** Interface for holding the service account pair for a given Backend. */ +export interface ServiceAccounts { + buildServiceAccount: string; + runServiceAccount: string; +} + +/** + * Interface for holding a collection of service accounts we need to grant access to. + * Build accounts are special because they also need secret viewer permissions to view versions + * and pin to the latest. Run accounts only need version accessor. + */ +export interface MultiServiceAccounts { + buildServiceAccounts: string[]; + runServiceAccounts: string[]; +} + +/** Utility function to turn a single ServiceAccounts into a MultiServiceAccounts. */ +export function toMulti(accounts: ServiceAccounts): MultiServiceAccounts { + const m: MultiServiceAccounts = { + buildServiceAccounts: [accounts.buildServiceAccount], + runServiceAccounts: [], + }; + if (accounts.buildServiceAccount !== accounts.runServiceAccount) { + m.runServiceAccounts.push(accounts.runServiceAccount); + } + return m; +} + +/** + * Finds the explicit service account used for a backend or, for legacy cases, + * the defaults for GCB and compute. + */ +export function serviceAccountsForBackend( + projectNumber: string, + backend: apphosting.Backend, +): ServiceAccounts { + if (backend.serviceAccount) { + return { + buildServiceAccount: backend.serviceAccount, + runServiceAccount: backend.serviceAccount, + }; + } + return { + buildServiceAccount: gcb.getDefaultServiceAccount(projectNumber), + runServiceAccount: gce.getDefaultServiceAccount(projectNumber), + }; +} + +/** + * Grants the corresponding service accounts the necessary access permissions to the provided secret. + */ +export async function grantSecretAccess( + projectId: string, + projectNumber: string, + secretName: string, + accounts: MultiServiceAccounts, +): Promise { + const p4saEmail = apphosting.serviceAgentEmail(projectNumber); + const newBindings: iam.Binding[] = [ + { + role: "roles/secretmanager.secretAccessor", + members: [...accounts.buildServiceAccounts, ...accounts.runServiceAccounts].map( + (sa) => `serviceAccount:${sa}`, + ), + }, + // Cloud Build needs the viewer role so that it can list secret versions and pin the Build to the + // latest version. + { + role: "roles/secretmanager.viewer", + members: accounts.buildServiceAccounts.map((sa) => `serviceAccount:${sa}`), + }, + // The App Hosting service agent needs the version manager role for automated garbage collection. + { + role: "roles/secretmanager.secretVersionManager", + members: [`serviceAccount:${p4saEmail}`], + }, + ]; + + let existingBindings; + try { + existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || []; + } catch (err: any) { + throw new FirebaseError( + `Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, + { original: err }, + ); + } + + try { + // TODO: Merge with existing bindings with the same role + const updatedBindings = existingBindings.concat(newBindings); + await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings); + } catch (err: any) { + throw new FirebaseError( + `Failed to set IAM bindings ${JSON.stringify(newBindings)} on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, + { original: err }, + ); + } + + utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`); +} + +/** + * Ensures a secret exists for use with app hosting, optionally locked to a region. + * If a secret exists, we verify the user is not trying to change the region and verifies a secret + * is not being used for both functions and app hosting as their garbage collection is incompatible + * (client vs server-side). + * @returns true if a secret was created, false if a secret already existed, and null if a user aborts. + */ +export async function upsertSecret( + project: string, + secret: string, + location?: string, +): Promise { + let existing: gcsm.Secret; + try { + existing = await gcsm.getSecret(project, secret); + } catch (err: any) { + if (err.status !== 404) { + throw new FirebaseError("Unexpected error loading secret", { original: err }); + } + await gcsm.createSecret(project, secret, gcsm.labels("apphosting"), location); + return true; + } + const replication = existing.replication?.userManaged; + if ( + location && + (replication?.replicas?.length !== 1 || replication?.replicas?.[0]?.location !== location) + ) { + utils.logLabeledError( + "apphosting", + "Secret replication policies cannot be changed after creation", + ); + return null; + } + if (isFunctionsManaged(existing)) { + utils.logLabeledWarning( + "apphosting", + `Cloud Functions for Firebase currently manages versions of ${secret}. Continuing will disable ` + + "automatic deletion of old versions.", + ); + const stopTracking = await prompt.confirm({ + message: "Do you wish to continue?", + default: false, + }); + if (!stopTracking) { + return null; + } + delete existing.labels[FIREBASE_MANAGED]; + await gcsm.patchSecret(project, secret, existing.labels); + } + // TODO: consider whether we should prompt a user who has an unmanaged secret to enroll in version control. + // This may not be a great idea until version control is actually implemented. + return false; +} diff --git a/src/archiveDirectory.js b/src/archiveDirectory.js deleted file mode 100644 index 421bf9fc56b..00000000000 --- a/src/archiveDirectory.js +++ /dev/null @@ -1,180 +0,0 @@ -"use strict"; - -const _ = require("lodash"); -const archiver = require("archiver"); -const filesize = require("filesize"); -const fs = require("fs"); -const path = require("path"); -const tar = require("tar"); -const tmp = require("tmp"); - -const { listFiles } = require("./listFiles"); -const { FirebaseError } = require("./error"); -const fsAsync = require("./fsAsync"); -const { logger } = require("./logger"); -const utils = require("./utils"); - -/** - * Archives a directory to a temporary file and returns information about the - * new archive. Defaults to type "tar", and returns a .tar.gz file. - * @param {string} sourceDirectory - * @param {object} options - * @param {string=} options.type Type of directory to create: "tar", or "zip". - * @param {Array=} options.ignore Globs to be ignored. - * @return {!Promise>} Information about the archive: - * - `file`: file name - * - `stream`: read stream of the archive - * - `manifest`: list of all files in the archive - * - `size`: information about the size of the archive - * - `source`: the source directory of the archive, for reference - */ -const archiveDirectory = (sourceDirectory, options) => { - options = options || {}; - let postfix = ".tar.gz"; - if (options.type === "zip") { - postfix = ".zip"; - } - const tempFile = tmp.fileSync({ - prefix: "firebase-archive-", - postfix, - }); - - if (!options.ignore) { - options.ignore = []; - } - - let makeArchive; - if (options.type === "zip") { - makeArchive = _zipDirectory(sourceDirectory, tempFile, options); - } else { - makeArchive = _tarDirectory(sourceDirectory, tempFile, options); - } - return makeArchive - .then((archive) => { - logger.debug(`Archived ${filesize(archive.size)} in ${sourceDirectory}.`); - return archive; - }) - .catch((err) => { - if (err instanceof FirebaseError) { - throw err; - } - return utils.reject("Failed to create archive.", { - original: err, - }); - }); -}; - -/** - * Archives a directory and returns information about the local archive. - * @param {string} sourceDirectory - * @return {!Object} Information with the following keys: - * - file: name of the temp archive that was created. - * - stream: read stream of the temp archive. - * - size: the size of the file. - */ -const _tarDirectory = (sourceDirectory, tempFile, options) => { - const allFiles = listFiles(sourceDirectory, options.ignore); - - // `tar` returns a `TypeError` if `allFiles` is empty. Let's check a feww things. - try { - fs.statSync(sourceDirectory); - } catch (err) { - if (err.code === "ENOENT") { - return utils.reject(`Could not read directory "${sourceDirectory}"`); - } - throw err; - } - if (!allFiles.length) { - return utils.reject( - `Cannot create a tar archive with 0 files from directory "${sourceDirectory}"` - ); - } - - return tar - .create( - { - gzip: true, - file: tempFile.name, - cwd: sourceDirectory, - follow: true, - noDirRecurse: true, - portable: true, - }, - allFiles - ) - .then(() => { - const stats = fs.statSync(tempFile.name); - return { - file: tempFile.name, - stream: fs.createReadStream(tempFile.name), - manifest: allFiles, - size: stats.size, - source: sourceDirectory, - }; - }); -}; - -/** - * Zips a directory and returns information about the local archive. - * @param {string} sourceDirectory - * @return {!Object} - */ -const _zipDirectory = (sourceDirectory, tempFile, options) => { - const archiveFileStream = fs.createWriteStream(tempFile.name, { - flags: "w", - encoding: "binary", - }); - const archive = archiver("zip"); - const archiveDone = _pipeAsync(archive, archiveFileStream); - const allFiles = []; - - return fsAsync - .readdirRecursive({ path: sourceDirectory, ignore: options.ignore }) - .catch((err) => { - if (err.code === "ENOENT") { - return utils.reject(`Could not read directory "${sourceDirectory}"`, { original: err }); - } - throw err; - }) - .then(function (files) { - _.forEach(files, function (file) { - const name = path.relative(sourceDirectory, file.name); - allFiles.push(name); - archive.file(file.name, { - name, - mode: file.mode, - }); - }); - archive.finalize(); - return archiveDone; - }) - .then(() => { - const stats = fs.statSync(tempFile.name); - return { - file: tempFile.name, - stream: fs.createReadStream(tempFile.name), - manifest: allFiles, - size: stats.size, - source: sourceDirectory, - }; - }); -}; - -/** - * Pipes one stream to another, resolving the returned promise on finish or - * rejects on an error. - * @param {!*} from a Stream - * @param {!*} to a Stream - * @return {!Promise} - */ -const _pipeAsync = function (from, to) { - return new Promise(function (resolve, reject) { - to.on("finish", resolve); - to.on("error", reject); - from.pipe(to); - }); -}; - -module.exports = { - archiveDirectory, -}; diff --git a/src/archiveDirectory.spec.ts b/src/archiveDirectory.spec.ts new file mode 100644 index 00000000000..5b0ac9cd7e3 --- /dev/null +++ b/src/archiveDirectory.spec.ts @@ -0,0 +1,18 @@ +import { resolve } from "path"; +import { expect } from "chai"; +import { FirebaseError } from "./error"; + +import { archiveDirectory } from "./archiveDirectory"; +import { FIXTURE_DIR } from "./test/fixtures/config-imports"; + +describe("archiveDirectory", () => { + it("should archive happy little directories", async () => { + const result = await archiveDirectory(FIXTURE_DIR, {}); + expect(result.source).to.equal(FIXTURE_DIR); + expect(result.size).to.be.greaterThan(0); + }); + + it("should throw a happy little error if the directory doesn't exist", async () => { + await expect(archiveDirectory(resolve(__dirname, "foo"), {})).to.be.rejectedWith(FirebaseError); + }); +}); diff --git a/src/archiveDirectory.ts b/src/archiveDirectory.ts new file mode 100644 index 00000000000..46a6945a83e --- /dev/null +++ b/src/archiveDirectory.ts @@ -0,0 +1,174 @@ +import * as archiver from "archiver"; +import * as filesize from "filesize"; +import * as fs from "fs"; +import * as path from "path"; +import * as tar from "tar"; +import * as tmp from "tmp"; + +import { FirebaseError } from "./error"; +import { listFiles } from "./listFiles"; +import { logger } from "./logger"; +import { Readable, Writable } from "stream"; +import * as fsAsync from "./fsAsync"; + +export interface ArchiveOptions { + /** Type of archive to create. */ + type?: "tar" | "zip"; + /** Globs to be ignored. */ + ignore?: string[]; +} + +export interface ArchiveResult { + /** File name. */ + file: string; + /** Read stream of the archive. */ + stream: Readable; + /** List of all the files in the archive. */ + manifest: string[]; + /** The size of the archive. */ + size: number; + /** The source directory of the archive. */ + source: string; +} + +/** + * Archives a directory to a temporary file and returns information about the + * new archive. Defaults to type "tar", and returns a .tar.gz file. + */ +export async function archiveDirectory( + sourceDirectory: string, + options: ArchiveOptions = {}, +): Promise { + let postfix = ".tar.gz"; + if (options.type === "zip") { + postfix = ".zip"; + } + const tempFile = tmp.fileSync({ + prefix: "firebase-archive-", + postfix, + }); + + if (!options.ignore) { + options.ignore = []; + } + + let makeArchive; + if (options.type === "zip") { + makeArchive = zipDirectory(sourceDirectory, tempFile, options); + } else { + makeArchive = tarDirectory(sourceDirectory, tempFile, options); + } + try { + const archive = await makeArchive; + logger.debug(`Archived ${filesize(archive.size)} in ${sourceDirectory}.`); + return archive; + } catch (err: any) { + if (err instanceof FirebaseError) { + throw err; + } + throw new FirebaseError("Failed to create archive.", { original: err }); + } +} + +/** + * Archives a directory and returns information about the local archive. + */ +async function tarDirectory( + sourceDirectory: string, + tempFile: tmp.FileResult, + options: ArchiveOptions, +): Promise { + const allFiles = listFiles(sourceDirectory, options.ignore); + + // `tar` returns a `TypeError` if `allFiles` is empty. Let's check a feww things. + try { + fs.statSync(sourceDirectory); + } catch (err: any) { + if (err.code === "ENOENT") { + throw new FirebaseError(`Could not read directory "${sourceDirectory}"`); + } + throw err; + } + if (!allFiles.length) { + throw new FirebaseError( + `Cannot create a tar archive with 0 files from directory "${sourceDirectory}"`, + ); + } + + await tar.create( + { + gzip: true, + file: tempFile.name, + cwd: sourceDirectory, + follow: true, + noDirRecurse: true, + portable: true, + }, + allFiles, + ); + const stats = fs.statSync(tempFile.name); + return { + file: tempFile.name, + stream: fs.createReadStream(tempFile.name), + manifest: allFiles, + size: stats.size, + source: sourceDirectory, + }; +} + +/** + * Zips a directory and returns information about the local archive. + */ +async function zipDirectory( + sourceDirectory: string, + tempFile: tmp.FileResult, + options: ArchiveOptions, +): Promise { + const archiveFileStream = fs.createWriteStream(tempFile.name, { + flags: "w", + encoding: "binary", + }); + const archive = archiver("zip"); + const archiveDone = pipeAsync(archive, archiveFileStream); + const allFiles: string[] = []; + + let files: fsAsync.ReaddirRecursiveFile[]; + try { + files = await fsAsync.readdirRecursive({ path: sourceDirectory, ignore: options.ignore }); + } catch (err: any) { + if (err.code === "ENOENT") { + throw new FirebaseError(`Could not read directory "${sourceDirectory}"`, { original: err }); + } + throw err; + } + for (const file of files) { + const name = path.relative(sourceDirectory, file.name); + allFiles.push(name); + archive.file(file.name, { + name, + mode: file.mode, + }); + } + void archive.finalize(); + await archiveDone; + const stats = fs.statSync(tempFile.name); + return { + file: tempFile.name, + stream: fs.createReadStream(tempFile.name), + manifest: allFiles, + size: stats.size, + source: sourceDirectory, + }; +} + +/** + * Pipes one stream to another, resolving the returned promise on finish or + * rejects on an error. + */ +async function pipeAsync(from: Readable, to: Writable): Promise { + return new Promise((resolve, reject) => { + to.on("finish", resolve); + to.on("error", reject); + from.pipe(to); + }); +} diff --git a/src/auth.spec.ts b/src/auth.spec.ts new file mode 100644 index 00000000000..ff02ae6ff8c --- /dev/null +++ b/src/auth.spec.ts @@ -0,0 +1,151 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { + getAdditionalAccounts, + getAllAccounts, + getGlobalDefaultAccount, + getProjectDefaultAccount, + selectAccount, +} from "./auth"; +import { configstore } from "./configstore"; +import { Account } from "./types/auth"; + +describe("auth", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let fakeConfigStore: any = {}; + + beforeEach(() => { + const configstoreGetStub = sandbox.stub(configstore, "get"); + configstoreGetStub.callsFake((key: string) => { + return fakeConfigStore[key]; + }); + + const configstoreSetStub = sandbox.stub(configstore, "set"); + configstoreSetStub.callsFake((...values: any) => { + fakeConfigStore[values[0]] = values[1]; + }); + + const configstoreDeleteStub = sandbox.stub(configstore, "delete"); + configstoreDeleteStub.callsFake((key: string) => { + delete fakeConfigStore[key]; + }); + }); + + afterEach(() => { + fakeConfigStore = {}; + sandbox.restore(); + }); + + describe("no accounts", () => { + it("returns no global account when config is empty", () => { + const account = getGlobalDefaultAccount(); + expect(account).to.be.undefined; + }); + }); + + describe("single account", () => { + const defaultAccount: Account = { + user: { + email: "test@test.com", + }, + tokens: { + access_token: "abc1234", + }, + }; + + beforeEach(() => { + configstore.set("user", defaultAccount.user); + configstore.set("tokens", defaultAccount.tokens); + }); + + it("returns global default account", () => { + const account = getGlobalDefaultAccount(); + expect(account).to.deep.equal(defaultAccount); + }); + + it("returns no additional accounts", () => { + const additional = getAdditionalAccounts(); + expect(additional.length).to.equal(0); + }); + + it("returns exactly one total account", () => { + const all = getAllAccounts(); + expect(all.length).to.equal(1); + expect(all[0]).to.deep.equal(defaultAccount); + }); + }); + + describe("multi account", () => { + const defaultAccount: Account = { + user: { + email: "test@test.com", + }, + tokens: { + access_token: "abc1234", + }, + }; + + const additionalUser1: Account = { + user: { + email: "test1@test.com", + }, + tokens: { + access_token: "token1", + }, + }; + + const additionalUser2: Account = { + user: { + email: "test2@test.com", + }, + tokens: { + access_token: "token2", + }, + }; + + const additionalAccounts: Account[] = [additionalUser1, additionalUser2]; + + const activeAccounts = { + "/path/project1": "test1@test.com", + }; + + beforeEach(() => { + configstore.set("user", defaultAccount.user); + configstore.set("tokens", defaultAccount.tokens); + configstore.set("additionalAccounts", additionalAccounts); + configstore.set("activeAccounts", activeAccounts); + }); + + it("returns global default account", () => { + const account = getGlobalDefaultAccount(); + expect(account).to.deep.equal(defaultAccount); + }); + + it("returns additional accounts", () => { + const additional = getAdditionalAccounts(); + expect(additional).to.deep.equal(additionalAccounts); + }); + + it("returns all accounts", () => { + const all = getAllAccounts(); + expect(all).to.deep.equal([defaultAccount, ...additionalAccounts]); + }); + + it("respects project default when present", () => { + const account = getProjectDefaultAccount("/path/project1"); + expect(account).to.deep.equal(additionalUser1); + }); + + it("ignores project default when not present", () => { + const account = getProjectDefaultAccount("/path/project2"); + expect(account).to.deep.equal(defaultAccount); + }); + + it("prefers account flag to project root", () => { + const account = selectAccount("test2@test.com", "/path/project1"); + expect(account).to.deep.equal(additionalUser2); + }); + }); +}); diff --git a/src/auth.ts b/src/auth.ts index e1f938dce29..b01320e6b56 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,14 +1,11 @@ -import * as clc from "cli-color"; -import * as fs from "fs"; -import * as jwt from "jsonwebtoken"; +import * as clc from "colorette"; +import * as FormData from "form-data"; import * as http from "http"; +import * as jwt from "jsonwebtoken"; import * as opn from "open"; -import * as path from "path"; import * as portfinder from "portfinder"; import * as url from "url"; -import * as util from "util"; -import * as api from "./api"; import * as apiv2 from "./apiv2"; import { configstore } from "./configstore"; import { FirebaseError } from "./error"; @@ -17,64 +14,32 @@ import { logger } from "./logger"; import { promptOnce } from "./prompt"; import * as scopes from "./scopes"; import { clearCredentials } from "./defaultCredentials"; - -/* eslint-disable camelcase */ -// The wire protocol for an access token returned by Google. -// When we actually refresh from the server we should always have -// these optional fields, but when a user passes --token we may -// only have access_token. -export interface Tokens { - id_token?: string; - access_token: string; - refresh_token?: string; - scopes?: string[]; -} - -export interface User { - email: string; - - iss?: string; - azp?: string; - aud?: string; - sub?: number; - hd?: string; - email_verified?: boolean; - at_hash?: string; - iat?: number; - exp?: number; -} - -export interface Account { - user: User; - tokens: Tokens; -} - -interface TokensWithExpiration extends Tokens { - expires_at?: number; -} - -interface TokensWithTTL extends Tokens { - expires_in?: number; -} - -interface UserCredentials { - user: string | User; - tokens: TokensWithExpiration; - scopes: string[]; -} - -// https://docs.github.com/en/developers/apps/authorizing-oauth-apps -interface GitHubAuthResponse { - access_token: string; - scope: string; - token_type: string; -} -/* eslint-enable camelcase */ - -// Typescript emulates modules, which have constant exports. We can -// overcome this by casting to any -// TODO fix after https://github.com/http-party/node-portfinder/pull/115 -((portfinder as unknown) as { basePort: number }).basePort = 9005; +import { v4 as uuidv4 } from "uuid"; +import { randomBytes, createHash } from "crypto"; +import { trackGA4 } from "./track"; +import { + authOrigin, + authProxyOrigin, + clientId, + clientSecret, + githubClientId, + githubClientSecret, + githubOrigin, + googleOrigin, +} from "./api"; +import { + Account, + AuthError, + User, + Tokens, + TokensWithExpiration, + TokensWithTTL, + GitHubAuthResponse, + UserCredentials, +} from "./types/auth"; +import { readTemplate } from "./templates"; + +portfinder.setBasePort(9005); /** * Get the global default account. Before multi-auth was implemented @@ -159,7 +124,6 @@ export function setActiveAccount(options: any, account: Account) { * @param token refresh token string */ export function setRefreshToken(token: string) { - api.setRefreshToken(token); apiv2.setRefreshToken(token); } @@ -188,7 +152,7 @@ export function selectAccount(account?: string, projectRoot?: string): Account | } throw new FirebaseError( - `Account ${account} not found, run "firebase login:list" to see existing accounts or "firebase login:add" to add a new one` + `Account ${account} not found, run "firebase login:list" to see existing accounts or "firebase login:add" to add a new one`, ); } @@ -223,9 +187,7 @@ export async function loginAdditionalAccount(useLocalhost: boolean, email?: stri utils.logWarning(`Already logged in as ${resultEmail}.`); updateAccount(newAccount); } else { - const additionalAccounts = getAdditionalAccounts(); - additionalAccounts.push(newAccount); - configstore.set("additionalAccounts", additionalAccounts); + addAdditionalAccount(newAccount); } return newAccount; @@ -273,7 +235,7 @@ function invalidCredentialError(): FirebaseError { "\n\n" + "For CI servers and headless environments, generate a new token with " + clc.bold("firebase login:ci"), - { exit: 1 } + { exit: 1 }, ); } @@ -311,10 +273,10 @@ function queryParamString(args: { [key: string]: string | undefined }) { function getLoginUrl(callbackUrl: string, userHint?: string) { return ( - api.authOrigin + + authOrigin() + "/o/oauth2/auth?" + queryParamString({ - client_id: api.clientId, + client_id: clientId(), scope: SCOPES.join(" "), response_type: "code", state: _nonce, @@ -324,24 +286,38 @@ function getLoginUrl(callbackUrl: string, userHint?: string) { ); } -async function getTokensFromAuthorizationCode(code: string, callbackUrl: string) { - let res: { - body?: TokensWithTTL; - statusCode: number; +async function getTokensFromAuthorizationCode( + code: string, + callbackUrl: string, + verifier?: string, +) { + const params: Record = { + code: code, + client_id: clientId(), + client_secret: clientSecret(), + redirect_uri: callbackUrl, + grant_type: "authorization_code", }; + if (verifier) { + params["code_verifier"] = verifier; + } + + let res: apiv2.ClientResponse; try { - res = await api.request("POST", "/o/oauth2/token", { - origin: api.authOrigin, - form: { - code: code, - client_id: api.clientId, - client_secret: api.clientSecret, - redirect_uri: callbackUrl, - grant_type: "authorization_code", - }, + const client = new apiv2.Client({ urlPrefix: authOrigin(), auth: false }); + const form = new FormData(); + for (const [k, v] of Object.entries(params)) { + form.append(k, v); + } + res = await client.request({ + method: "POST", + path: "/o/oauth2/token", + body: form, + headers: form.getHeaders(), + skipLog: { body: true, queryParams: true, resBody: true }, }); - } catch (err) { + } catch (err: any) { if (err instanceof Error) { logger.debug("Token Fetch Error:", err.stack || ""); } else { @@ -349,15 +325,15 @@ async function getTokensFromAuthorizationCode(code: string, callbackUrl: string) } throw invalidCredentialError(); } - if (!res?.body?.access_token && !res?.body?.refresh_token) { - logger.debug("Token Fetch Error:", res.statusCode, res.body); + if (!res.body.access_token && !res.body.refresh_token) { + logger.debug("Token Fetch Error:", res.status, res.body); throw invalidCredentialError(); } lastAccessToken = Object.assign( { - expires_at: Date.now() + res!.body!.expires_in! * 1000, + expires_at: Date.now() + res.body.expires_in! * 1000, }, - res.body + res.body, ); return lastAccessToken; } @@ -366,10 +342,10 @@ const GITHUB_SCOPES = ["read:user", "repo", "public_repo"]; function getGithubLoginUrl(callbackUrl: string) { return ( - api.githubOrigin + + githubOrigin() + "/login/oauth/authorize?" + queryParamString({ - client_id: api.githubClientId, + client_id: githubClientId(), state: _nonce, redirect_uri: callbackUrl, scope: GITHUB_SCOPES.join(" "), @@ -378,75 +354,121 @@ function getGithubLoginUrl(callbackUrl: string) { } async function getGithubTokensFromAuthorizationCode(code: string, callbackUrl: string) { - const res: { body: GitHubAuthResponse } = await api.request("POST", "/login/oauth/access_token", { - origin: api.githubOrigin, - form: { - client_id: api.githubClientId, - client_secret: api.githubClientSecret, - code, - redirect_uri: callbackUrl, - state: _nonce, - }, + const client = new apiv2.Client({ urlPrefix: githubOrigin(), auth: false }); + const data = { + client_id: githubClientId(), + client_secret: githubClientSecret(), + code, + redirect_uri: callbackUrl, + state: _nonce, + }; + const form = new FormData(); + for (const [k, v] of Object.entries(data)) { + form.append(k, v); + } + const headers = form.getHeaders(); + headers.accept = "application/json"; + const res = await client.request({ + method: "POST", + path: "/login/oauth/access_token", + body: form, + headers, }); - return res.body.access_token as string; + return res.body.access_token; } -async function respondWithFile( +function respondHtml( req: http.IncomingMessage, res: http.ServerResponse, statusCode: number, - filename: string -) { - const response = await util.promisify(fs.readFile)(path.join(__dirname, filename)); + html: string, +): void { res.writeHead(statusCode, { - "Content-Length": response.length, + "Content-Length": html.length, "Content-Type": "text/html", }); - res.end(response); + res.end(html); req.socket.destroy(); } -async function loginWithoutLocalhost(userHint?: string): Promise { - const callbackUrl = getCallbackUrl(); - const authUrl = getLoginUrl(callbackUrl, userHint); +function urlsafeBase64(base64string: string) { + return base64string.replace(/\+/g, "-").replace(/=+$/, "").replace(/\//g, "_"); +} + +async function loginRemotely(): Promise { + const authProxyClient = new apiv2.Client({ + urlPrefix: authProxyOrigin(), + auth: false, + }); + + const sessionId = uuidv4(); + const codeVerifier = randomBytes(32).toString("hex"); + // urlsafe base64 is required for code_challenge in OAuth PKCE + const codeChallenge = urlsafeBase64(createHash("sha256").update(codeVerifier).digest("base64")); + + const attestToken = ( + await authProxyClient.post<{ session_id: string }, { token: string }>("/attest", { + session_id: sessionId, + }) + ).body?.token; + + const loginUrl = `${authProxyOrigin()}/login?code_challenge=${codeChallenge}&session=${sessionId}&attest=${attestToken}`; logger.info(); - logger.info("Visit this URL on any device to log in:"); - logger.info(clc.bold.underline(authUrl)); + logger.info("To sign in to the Firebase CLI:"); + logger.info(); + logger.info("1. Take note of your session ID:"); + logger.info(); + logger.info(` ${clc.bold(sessionId.substring(0, 5).toUpperCase())}`); + logger.info(); + logger.info("2. Visit the URL below on any device and follow the instructions to get your code:"); + logger.info(); + logger.info(` ${loginUrl}`); + logger.info(); + logger.info("3. Paste or enter the authorization code below once you have it:"); logger.info(); - open(authUrl); - - const code: string = await promptOnce({ + const code = await promptOnce({ type: "input", - name: "code", - message: "Paste authorization code here:", + message: "Enter authorization code:", }); - const tokens = await getTokensFromAuthorizationCode(code, callbackUrl); - // getTokensFromAuthorizationCode doesn't handle the --token case, so we know - // that we'll have a valid id_token. - return { - user: jwt.decode(tokens.id_token!) as User, - tokens: tokens, - scopes: SCOPES, - }; + + try { + const tokens = await getTokensFromAuthorizationCode( + code, + `${authProxyOrigin()}/complete`, + codeVerifier, + ); + + void trackGA4("login", { method: "google_remote" }); + + return { + user: jwt.decode(tokens.id_token!, { json: true }) as any as User, + tokens: tokens, + scopes: SCOPES, + }; + } catch (e) { + throw new FirebaseError("Unable to authenticate using the provided code. Please try again."); + } } async function loginWithLocalhostGoogle(port: number, userHint?: string): Promise { const callbackUrl = getCallbackUrl(port); const authUrl = getLoginUrl(callbackUrl, userHint); - const successTemplate = "../templates/loginSuccess.html"; + const successHtml = await readTemplate("loginSuccess.html"); const tokens = await loginWithLocalhost( port, callbackUrl, authUrl, - successTemplate, - getTokensFromAuthorizationCode + successHtml, + getTokensFromAuthorizationCode, ); + + void trackGA4("login", { method: "google_localhost" }); // getTokensFromAuthoirzationCode doesn't handle the --token case, so we know we'll // always have an id_token. return { - user: jwt.decode(tokens.id_token!) as User, + user: jwt.decode(tokens.id_token!, { json: true }) as any as User, tokens: tokens, scopes: tokens.scopes!, }; @@ -455,32 +477,34 @@ async function loginWithLocalhostGoogle(port: number, userHint?: string): Promis async function loginWithLocalhostGitHub(port: number): Promise { const callbackUrl = getCallbackUrl(port); const authUrl = getGithubLoginUrl(callbackUrl); - const successTemplate = "../templates/loginSuccessGithub.html"; - return loginWithLocalhost( + const successHtml = await readTemplate("loginSuccessGithub.html"); + const tokens = await loginWithLocalhost( port, callbackUrl, authUrl, - successTemplate, - getGithubTokensFromAuthorizationCode + successHtml, + getGithubTokensFromAuthorizationCode, ); + void trackGA4("login", { method: "github_localhost" }); + return tokens; } async function loginWithLocalhost( port: number, callbackUrl: string, authUrl: string, - successTemplate: string, - getTokens: (queryCode: string, callbackUrl: string) => Promise + successHtml: string, + getTokens: (queryCode: string, callbackUrl: string) => Promise, ): Promise { return new Promise((resolve, reject) => { const server = http.createServer(async (req, res) => { - let tokens: Tokens; const query = url.parse(`${req.url}`, true).query || {}; const queryState = query.state; const queryCode = query.code; if (queryState !== _nonce || typeof queryCode !== "string") { - await respondWithFile(req, res, 400, "../templates/loginFailure.html"); + const html = await readTemplate("loginFailure.html"); + respondHtml(req, res, 400, html); reject(new FirebaseError("Unexpected error while logging in")); server.close(); return; @@ -488,10 +512,11 @@ async function loginWithLocalhost( try { const tokens = await getTokens(queryCode, callbackUrl); - await respondWithFile(req, res, 200, successTemplate); + respondHtml(req, res, 200, successHtml); resolve(tokens); - } catch (err) { - await respondWithFile(req, res, 400, "../templates/loginFailure.html"); + } catch (err: any) { + const html = await readTemplate("loginFailure.html"); + respondHtml(req, res, 400, html); reject(err); } server.close(); @@ -501,7 +526,7 @@ async function loginWithLocalhost( server.listen(port, () => { logger.info(); logger.info("Visit this URL on this device to log in:"); - logger.info(clc.bold.underline(authUrl)); + logger.info(clc.bold(clc.underline(authUrl))); logger.info(); logger.info("Waiting for authentication..."); @@ -516,15 +541,14 @@ async function loginWithLocalhost( export async function loginGoogle(localhost: boolean, userHint?: string): Promise { if (localhost) { - const port = await getPort(); try { const port = await getPort(); return await loginWithLocalhostGoogle(port, userHint); } catch { - return await loginWithoutLocalhost(userHint); + return await loginRemotely(); } } - return await loginWithoutLocalhost(userHint); + return await loginRemotely(); } export async function loginGithub(): Promise { @@ -611,37 +635,55 @@ function logoutCurrentSession(refreshToken: string) { async function refreshTokens( refreshToken: string, - authScopes: string[] + authScopes: string[], ): Promise { logger.debug("> refreshing access token with scopes:", JSON.stringify(authScopes)); try { - const res = await api.request("POST", "/oauth2/v3/token", { - origin: api.googleOrigin, - form: { - refresh_token: refreshToken, - client_id: api.clientId, - client_secret: api.clientSecret, - grant_type: "refresh_token", - scope: (authScopes || []).join(" "), - }, - logOptions: { skipRequestBody: true, skipQueryParams: true, skipResponseBody: true }, + const client = new apiv2.Client({ urlPrefix: googleOrigin(), auth: false }); + const data = { + refresh_token: refreshToken, + client_id: clientId(), + client_secret: clientSecret(), + grant_type: "refresh_token", + scope: (authScopes || []).join(" "), + }; + const form = new FormData(); + for (const [k, v] of Object.entries(data)) { + form.append(k, v); + } + const res = await client.request({ + method: "POST", + path: "/oauth2/v3/token", + body: form, + headers: form.getHeaders(), + skipLog: { body: true, queryParams: true, resBody: true }, + resolveOnHTTPError: true, }); + const forceReauthErrs: AuthError[] = [ + { error: "invalid_grant", error_subtype: "invalid_rapt" }, // Cloud Session Control expiry + ]; + const matches = (a: AuthError, b: AuthError) => { + return a.error === b.error && a.error_subtype === b.error_subtype; + }; + if (forceReauthErrs.some((a) => matches(a, res.body))) { + throw invalidCredentialError(); + } if (res.status === 401 || res.status === 400) { // Support --token commands. In this case we won't have an expiration // time, scopes, etc. return { access_token: refreshToken }; } - if (typeof res.body?.access_token !== "string") { + if (typeof res.body.access_token !== "string") { throw invalidCredentialError(); } lastAccessToken = Object.assign( { - expires_at: Date.now() + res.body.expires_in * 1000, + expires_at: Date.now() + res.body.expires_in! * 1000, refresh_token: refreshToken, scopes: authScopes, }, - res.body + res.body, ); const account = findAccountByRefreshToken(refreshToken); @@ -651,7 +693,7 @@ async function refreshTokens( } return lastAccessToken!; - } catch (err) { + } catch (err: any) { if (err?.context?.body?.error === "invalid_scope") { throw new FirebaseError( "This command requires new authorization scopes not granted to your current session. Please run " + @@ -659,7 +701,7 @@ async function refreshTokens( "\n\n" + "For CI servers and headless environments, generate a new token with " + clc.bold("firebase login:ci"), - { exit: 1 } + { exit: 1 }, ); } @@ -667,11 +709,10 @@ async function refreshTokens( } } -export async function getAccessToken(refreshToken: string, authScopes: string[]) { - if (haveValidTokens(refreshToken, authScopes)) { +export async function getAccessToken(refreshToken: string, authScopes: string[]): Promise { + if (haveValidTokens(refreshToken, authScopes) && lastAccessToken) { return lastAccessToken; } - return refreshTokens(refreshToken, authScopes); } @@ -681,13 +722,9 @@ export async function logout(refreshToken: string) { } logoutCurrentSession(refreshToken); try { - await api.request("GET", "/o/oauth2/revoke", { - origin: api.authOrigin, - data: { - token: refreshToken, - }, - }); - } catch (thrown) { + const client = new apiv2.Client({ urlPrefix: authOrigin(), auth: false }); + await client.get("/o/oauth2/revoke", { queryParams: { token: refreshToken } }); + } catch (thrown: any) { const err: Error = thrown instanceof Error ? thrown : new Error(thrown); throw new FirebaseError("Authentication Error.", { exit: 1, @@ -695,3 +732,13 @@ export async function logout(refreshToken: string) { }); } } + +/** + * adds an account to the list of additional accounts. + * @param account the account to add. + */ +export function addAdditionalAccount(account: Account): void { + const additionalAccounts = getAdditionalAccounts(); + additionalAccounts.push(account); + configstore.set("additionalAccounts", additionalAccounts); +} diff --git a/src/bin/firebase.js b/src/bin/firebase.js deleted file mode 100755 index 7540f3d7aa0..00000000000 --- a/src/bin/firebase.js +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -// Make check for Node 8, which is no longer supported by the CLI. -const semver = require("semver"); -const pkg = require("../../package.json"); -const nodeVersion = process.version; -if (!semver.satisfies(nodeVersion, pkg.engines.node)) { - console.error( - "Firebase CLI v" + - pkg.version + - " is incompatible with Node.js " + - nodeVersion + - " Please upgrade Node.js to version " + - pkg.engines.node - ); - process.exit(1); -} - -const updateNotifier = require("update-notifier")({ pkg: pkg }); -const clc = require("cli-color"); -const TerminalRenderer = require("marked-terminal"); -const marked = require("marked"); -marked.setOptions({ - renderer: new TerminalRenderer(), -}); -const updateMessage = - `Update available ${clc.xterm(240)("{currentVersion}")} → ${clc.green("{latestVersion}")}\n` + - `To update to the latest version using npm, run ${clc.cyan("npm install -g firebase-tools")}\n` + - `For other CLI management options, visit the ${marked( - "[CLI documentation](https://firebase.google.com/docs/cli#update-cli)" - )}`; -updateNotifier.notify({ defer: true, isGlobal: true, message: updateMessage }); - -const client = require(".."); -const errorOut = require("../errorOut").errorOut; -const winston = require("winston"); -const { SPLAT } = require("triple-beam"); -const { logger } = require("../logger"); -const fs = require("fs"); -const fsutils = require("../fsutils"); -const path = require("path"); -const ansiStrip = require("cli-color/strip"); -const { configstore } = require("../configstore"); -const _ = require("lodash"); -let args = process.argv.slice(2); -const { handlePreviewToggles } = require("../handlePreviewToggles"); -const utils = require("../utils"); -let cmd; - -function findAvailableLogFile() { - const candidates = ["firebase-debug.log"]; - for (let i = 1; i < 10; i++) { - candidates.push(`firebase-debug.${i}.log`); - } - - for (const c of candidates) { - const logFilename = path.join(process.cwd(), c); - - try { - const fd = fs.openSync(logFilename, "r+"); - fs.closeSync(fd); - return logFilename; - } catch (e) { - if (e.code === "ENOENT") { - // File does not exist, which is fine - return logFilename; - } - - // Any other error (EPERM, etc) means we won't be able to log to - // this file so we skip it. - } - } - - throw new Error("Unable to obtain permissions for firebase-debug.log"); -} - -const logFilename = findAvailableLogFile(); - -if (!process.env.DEBUG && _.includes(args, "--debug")) { - process.env.DEBUG = "true"; -} - -process.env.IS_FIREBASE_CLI = "true"; - -logger.add( - new winston.transports.File({ - level: "debug", - filename: logFilename, - format: winston.format.printf((info) => { - const segments = [info.message, ...(info[SPLAT] || [])].map(utils.tryStringify); - return `[${info.level}] ${ansiStrip(segments.join(" "))}`; - }), - }) -); - -logger.debug(_.repeat("-", 70)); -logger.debug("Command: ", process.argv.join(" ")); -logger.debug("CLI Version: ", pkg.version); -logger.debug("Platform: ", process.platform); -logger.debug("Node Version: ", process.version); -logger.debug("Time: ", new Date().toString()); -if (utils.envOverrides.length) { - logger.debug("Env Overrides:", utils.envOverrides.join(", ")); -} -logger.debug(_.repeat("-", 70)); -logger.debug(); - -require("../fetchMOTD").fetchMOTD(); - -process.on("exit", function (code) { - code = process.exitCode || code; - if (!process.env.DEBUG && code < 2 && fsutils.fileExistsSync(logFilename)) { - fs.unlinkSync(logFilename); - } - - if (code > 0 && process.stdout.isTTY) { - const lastError = configstore.get("lastError") || 0; - const timestamp = Date.now(); - if (lastError > timestamp - 120000) { - let help; - if (code === 1 && cmd) { - const commandName = _.get(_.last(cmd.args), "_name", "[command]"); - help = "Having trouble? Try " + clc.bold("firebase " + commandName + " --help"); - } else { - help = "Having trouble? Try again or contact support with contents of firebase-debug.log"; - } - - if (cmd) { - console.log(); - console.log(help); - } - } - configstore.set("lastError", timestamp); - } else { - configstore.delete("lastError"); - } -}); -require("exit-code"); - -process.on("uncaughtException", function (err) { - errorOut(err); -}); - -if (!handlePreviewToggles(args)) { - cmd = client.cli.parse(process.argv); - - // determine if there are any non-option arguments. if not, display help - args = args.filter(function (arg) { - return arg.indexOf("-") < 0; - }); - if (!args.length) { - client.cli.help(); - } -} diff --git a/src/bin/firebase.ts b/src/bin/firebase.ts new file mode 100755 index 00000000000..3bbac5ccbd0 --- /dev/null +++ b/src/bin/firebase.ts @@ -0,0 +1,170 @@ +#!/usr/bin/env node + +// Check for older versions of Node no longer supported by the CLI. +import * as semver from "semver"; +const pkg = require("../../package.json"); +const nodeVersion = process.version; +if (!semver.satisfies(nodeVersion, pkg.engines.node)) { + console.error( + `Firebase CLI v${pkg.version} is incompatible with Node.js ${nodeVersion} Please upgrade Node.js to version ${pkg.engines.node}`, + ); + process.exit(1); +} + +import * as updateNotifierPkg from "update-notifier-cjs"; +import * as clc from "colorette"; +import * as TerminalRenderer from "marked-terminal"; +const updateNotifier = updateNotifierPkg({ pkg }); +import { marked } from "marked"; +marked.setOptions({ + renderer: new TerminalRenderer(), +}); + +import { Command } from "commander"; +import { join } from "node:path"; +import { SPLAT } from "triple-beam"; +const stripAnsi = require("strip-ansi"); +import * as fs from "node:fs"; + +import { configstore } from "../configstore"; +import { errorOut } from "../errorOut"; +import { handlePreviewToggles } from "../handlePreviewToggles"; +import { logger } from "../logger"; +import * as client from ".."; +import * as fsutils from "../fsutils"; +import * as utils from "../utils"; +import * as winston from "winston"; + +let args = process.argv.slice(2); +let cmd: Command; + +function findAvailableLogFile(): string { + const candidates = ["firebase-debug.log"]; + for (let i = 1; i < 10; i++) { + candidates.push(`firebase-debug.${i}.log`); + } + + for (const c of candidates) { + const logFilename = join(process.cwd(), c); + + try { + const fd = fs.openSync(logFilename, "r+"); + fs.closeSync(fd); + return logFilename; + } catch (e: any) { + if (e.code === "ENOENT") { + // File does not exist, which is fine + return logFilename; + } + + // Any other error (EPERM, etc) means we won't be able to log to + // this file so we skip it. + } + } + + throw new Error("Unable to obtain permissions for firebase-debug.log"); +} + +const logFilename = findAvailableLogFile(); + +if (!process.env.DEBUG && args.includes("--debug")) { + process.env.DEBUG = "true"; +} + +process.env.IS_FIREBASE_CLI = "true"; + +logger.add( + new winston.transports.File({ + level: "debug", + filename: logFilename, + format: winston.format.printf((info) => { + const segments = [info.message, ...(info[SPLAT] || [])].map(utils.tryStringify); + return `[${info.level}] ${stripAnsi(segments.join(" "))}`; + }), + }), +); + +logger.debug("-".repeat(70)); +logger.debug("Command: ", process.argv.join(" ")); +logger.debug("CLI Version: ", pkg.version); +logger.debug("Platform: ", process.platform); +logger.debug("Node Version: ", process.version); +logger.debug("Time: ", new Date().toString()); +if (utils.envOverrides.length) { + logger.debug("Env Overrides:", utils.envOverrides.join(", ")); +} +logger.debug("-".repeat(70)); +logger.debug(); + +import { enableExperimentsFromCliEnvVariable } from "../experiments"; +import { fetchMOTD } from "../fetchMOTD"; + +enableExperimentsFromCliEnvVariable(); +fetchMOTD(); + +process.on("exit", (code) => { + code = process.exitCode || code; + if (!process.env.DEBUG && code < 2 && fsutils.fileExistsSync(logFilename)) { + fs.unlinkSync(logFilename); + } + + if (code > 0 && process.stdout.isTTY) { + const lastError = configstore.get("lastError") || 0; + const timestamp = Date.now(); + if (lastError > timestamp - 120000) { + let help; + if (code === 1 && cmd) { + help = "Having trouble? Try " + clc.bold("firebase [command] --help"); + } else { + help = "Having trouble? Try again or contact support with contents of firebase-debug.log"; + } + + if (cmd) { + console.log(); + console.log(help); + } + } + configstore.set("lastError", timestamp); + } else { + configstore.delete("lastError"); + } + + // Notify about updates right before process exit. + try { + const updateMessage = + `Update available ${clc.gray("{currentVersion}")} → ${clc.green("{latestVersion}")}\n` + + `To update to the latest version using npm, run\n${clc.cyan( + "npm install -g firebase-tools", + )}\n` + + `For other CLI management options, visit the ${marked( + "[CLI documentation](https://firebase.google.com/docs/cli#update-cli)", + )}`; + // `defer: true` would interfere with commands that perform tasks (emulators etc.) + // before exit since it installs a SIGINT handler that immediately exits. See: + // https://github.com/firebase/firebase-tools/issues/4981 + updateNotifier.notify({ defer: false, isGlobal: true, message: updateMessage }); + } catch (err) { + // This is not a fatal error -- let's debug log, swallow, and exit cleanly. + logger.debug("Error when notifying about new CLI updates:"); + if (err instanceof Error) { + logger.debug(err); + } else { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + logger.debug(`${err}`); + } + } +}); + +process.on("uncaughtException", (err) => { + errorOut(err); +}); + +if (!handlePreviewToggles(args)) { + cmd = client.cli.parse(process.argv); + + // determine if there are any non-option arguments. if not, display help + args = args.filter((arg) => !arg.includes("-")); + if (!args.length) { + client.cli.help(); + } +} diff --git a/src/test/checkMinRequiredVersion.spec.ts b/src/checkMinRequiredVersion.spec.ts similarity index 82% rename from src/test/checkMinRequiredVersion.spec.ts rename to src/checkMinRequiredVersion.spec.ts index 69cc83c440f..2c4da43929e 100644 --- a/src/test/checkMinRequiredVersion.spec.ts +++ b/src/checkMinRequiredVersion.spec.ts @@ -1,8 +1,8 @@ import { expect } from "chai"; -import { configstore } from "../configstore"; +import { configstore } from "./configstore"; import * as sinon from "sinon"; -import { checkMinRequiredVersion } from "../checkMinRequiredVersion"; +import { checkMinRequiredVersion } from "./checkMinRequiredVersion"; import Sinon from "sinon"; describe("checkMinRequiredVersion", () => { @@ -21,7 +21,7 @@ describe("checkMinRequiredVersion", () => { expect(() => { checkMinRequiredVersion({}, "key"); - }).to.throw; + }).to.throw(); }); it("should not error if installed version is above the min required version", () => { @@ -29,6 +29,6 @@ describe("checkMinRequiredVersion", () => { expect(() => { checkMinRequiredVersion({}, "key"); - }).not.to.throw; + }).not.to.throw(); }); }); diff --git a/src/checkMinRequiredVersion.ts b/src/checkMinRequiredVersion.ts index 0c5fd053bb9..943c8e09a8e 100644 --- a/src/checkMinRequiredVersion.ts +++ b/src/checkMinRequiredVersion.ts @@ -15,7 +15,7 @@ export function checkMinRequiredVersion(options: any, key: string) { const minVersion = configstore.get(`motd.${key}`); if (minVersion && semver.gt(minVersion, pkg.version)) { throw new FirebaseError( - `This command requires at least version ${minVersion} of the CLI to use. To update to the latest version using npm, run \`npm install -g firebase-tools\`. For other CLI management options, see https://firebase.google.com/docs/cli#update-cli` + `This command requires at least version ${minVersion} of the CLI to use. To update to the latest version using npm, run \`npm install -g firebase-tools\`. For other CLI management options, see https://firebase.google.com/docs/cli#update-cli`, ); } } diff --git a/src/checkValidTargetFilters.js b/src/checkValidTargetFilters.js deleted file mode 100644 index 71dfba843c0..00000000000 --- a/src/checkValidTargetFilters.js +++ /dev/null @@ -1,50 +0,0 @@ -"use strict"; - -var _ = require("lodash"); - -var { FirebaseError } = require("./error"); - -module.exports = function (options) { - function numFilters(targetTypes) { - return _.filter(options.only, function (opt) { - var optChunks = opt.split(":"); - return _.includes(targetTypes, optChunks[0]) && optChunks.length > 1; - }).length; - } - function targetContainsFilter(targetTypes) { - return numFilters(targetTypes) > 1; - } - function targetDoesNotContainFilter(targetTypes) { - return numFilters(targetTypes) === 0; - } - - return new Promise(function (resolve, reject) { - if (!options.only) { - return resolve(); - } - if (options.except) { - return reject( - new FirebaseError("Cannot specify both --only and --except", { - exit: 1, - }) - ); - } - if (targetContainsFilter(["database", "storage", "hosting"])) { - return reject( - new FirebaseError( - "Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions", - { exit: 1 } - ) - ); - } - if (targetContainsFilter(["functions"]) && targetDoesNotContainFilter(["functions"])) { - return reject( - new FirebaseError( - 'Cannot specify "--only functions" and "--only functions:" at the same time', - { exit: 1 } - ) - ); - } - return resolve(); - }); -}; diff --git a/src/checkValidTargetFilters.spec.ts b/src/checkValidTargetFilters.spec.ts new file mode 100644 index 00000000000..e77100fe7c3 --- /dev/null +++ b/src/checkValidTargetFilters.spec.ts @@ -0,0 +1,70 @@ +import { expect } from "chai"; + +import { Options } from "./options"; +import { RC } from "./rc"; + +import { checkValidTargetFilters } from "./checkValidTargetFilters"; + +const SAMPLE_OPTIONS: Options = { + cwd: "/", + configPath: "/", + /* eslint-disable-next-line */ + config: {} as any, + only: "", + except: "", + nonInteractive: false, + json: false, + interactive: false, + debug: false, + force: false, + filteredTargets: [], + rc: new RC(), +}; +const UNFILTERABLE_TARGETS = ["remoteconfig", "extensions"]; + +describe("checkValidTargetFilters", () => { + it("should resolve", async () => { + const options = Object.assign(SAMPLE_OPTIONS, { + only: "functions", + }); + await expect(checkValidTargetFilters(options)).to.be.fulfilled; + }); + + it("should resolve if there are no 'only' targets specified", async () => { + const options = Object.assign(SAMPLE_OPTIONS, { + only: null, + }); + await expect(checkValidTargetFilters(options)).to.be.fulfilled; + }); + + it("should error if an only option and except option have been provided", async () => { + const options = Object.assign(SAMPLE_OPTIONS, { + only: "functions", + except: "hosting", + }); + await expect(checkValidTargetFilters(options)).to.be.rejectedWith( + "Cannot specify both --only and --except", + ); + }); + + UNFILTERABLE_TARGETS.forEach((target) => { + it(`should error if non-filter-type target (${target}) has filters`, async () => { + const options = Object.assign(SAMPLE_OPTIONS, { + only: `${target}:filter`, + except: null, + }); + await expect(checkValidTargetFilters(options)).to.be.rejectedWith( + /Filters specified with colons \(e.g. --only functions:func1,functions:func2\) are only supported for .*/, + ); + }); + }); + + it("should error if the same target is specified with and without a filter", async () => { + const options = Object.assign(SAMPLE_OPTIONS, { + only: "functions,functions:filter", + }); + await expect(checkValidTargetFilters(options)).to.be.rejectedWith( + 'Cannot specify "--only functions" and "--only functions:" at the same time', + ); + }); +}); diff --git a/src/checkValidTargetFilters.ts b/src/checkValidTargetFilters.ts new file mode 100644 index 00000000000..211deeaf33d --- /dev/null +++ b/src/checkValidTargetFilters.ts @@ -0,0 +1,72 @@ +import { VALID_DEPLOY_TARGETS } from "./commands/deploy"; +import { FirebaseError } from "./error"; +import { Options } from "./options"; + +/** Returns targets from `only` only for the specified deploy types. */ +function targetsForTypes(only: string[], ...types: string[]): string[] { + return only.filter((t) => { + if (t.includes(":")) { + return types.includes(t.split(":")[0]); + } else { + return types.includes(t); + } + }); +} + +/** Returns true if any target has a filter (:). */ +function targetsHaveFilters(...targets: string[]): boolean { + return targets.some((t) => t.includes(":")); +} + +/** Returns true if any target doesn't include a filter (:). */ +function targetsHaveNoFilters(...targets: string[]): boolean { + return targets.some((t) => !t.includes(":")); +} + +const FILTERABLE_TARGETS = new Set([ + "hosting", + "functions", + "firestore", + "storage", + "database", + "dataconnect", +]); + +/** + * Validates that the target filters in options.only are valid. + * Throws an error (rejects) if it is invalid. + */ +export async function checkValidTargetFilters(options: Options): Promise { + const only = !options.only ? [] : options.only.split(","); + + return new Promise((resolve, reject) => { + if (!only.length) { + return resolve(); + } + if (options.except) { + return reject(new FirebaseError("Cannot specify both --only and --except")); + } + const nonFilteredTypes = VALID_DEPLOY_TARGETS.filter((t) => !FILTERABLE_TARGETS.has(t)); + const targetsForNonFilteredTypes = targetsForTypes(only, ...nonFilteredTypes); + if (targetsForNonFilteredTypes.length && targetsHaveFilters(...targetsForNonFilteredTypes)) { + return reject( + new FirebaseError( + "Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions, hosting, storage, and firestore", + ), + ); + } + const targetsForFunctions = targetsForTypes(only, "functions"); + if ( + targetsForFunctions.length && + targetsHaveFilters(...targetsForFunctions) && + targetsHaveNoFilters(...targetsForFunctions) + ) { + return reject( + new FirebaseError( + 'Cannot specify "--only functions" and "--only functions:" at the same time', + ), + ); + } + return resolve(); + }); +} diff --git a/src/test/command.spec.ts b/src/command.spec.ts similarity index 97% rename from src/test/command.spec.ts rename to src/command.spec.ts index 844b82b5959..465750d8180 100644 --- a/src/test/command.spec.ts +++ b/src/command.spec.ts @@ -1,8 +1,8 @@ import { expect } from "chai"; import * as nock from "nock"; -import { Command, validateProjectId } from "../command"; -import { FirebaseError } from "../error"; +import { Command, validateProjectId } from "./command"; +import { FirebaseError } from "./error"; describe("Command", () => { let command: Command; @@ -20,13 +20,13 @@ describe("Command", () => { (arr: string[]) => { return arr; }, - ["foo", "bar"] + ["foo", "bar"], ); command.help("here's how!"); command.action(() => { // do nothing }); - }).not.to.throw; + }).not.to.throw(); }); describe("runner", () => { diff --git a/src/command.ts b/src/command.ts index db70c24d726..7d400377367 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,15 +1,14 @@ -import { bold } from "cli-color"; +import * as clc from "colorette"; import { CommanderStatic } from "commander"; import { first, last, get, size, head, keys, values } from "lodash"; import { FirebaseError } from "./error"; -import { getInheritedOption, setupLoggers } from "./utils"; -import { loadRC, RC } from "./rc"; +import { getInheritedOption, setupLoggers, withTimeout } from "./utils"; +import { loadRC } from "./rc"; import { Config } from "./config"; import { configstore } from "./configstore"; import { detectProjectRoot } from "./detectProjectRoot"; -import track = require("./track"); -import clc = require("cli-color"); +import { trackEmulator, trackGA4 } from "./track"; import { selectAccount, setActiveAccount } from "./auth"; import { getFirebaseProject } from "./management/projects"; import { requireAuth } from "./requireAuth"; @@ -126,7 +125,7 @@ export class Command { } /** - * Registers the command with the client. This is used to inisially set up + * Registers the command with the client. This is used to initially set up * all the commands and wraps their functionality with analytics and error * handling. * @param client the client object (from src/index.js). @@ -145,6 +144,7 @@ export class Command { if (this.helpText) { cmd.on("--help", () => { + console.log(); // Seperates the help text from global options. console.log(this.helpText); }); } @@ -157,7 +157,7 @@ export class Command { // eslint-disable-next-line @typescript-eslint/no-explicit-any cmd.action((...args: any[]) => { const runner = this.runner(); - const start = new Date().getTime(); + const start = process.uptime(); const options = last(args); // We do not want to provide more arguments to the action functions than // we are able to - we're not sure what the ripple effects are. Our @@ -173,47 +173,99 @@ export class Command { if (args.length - 1 > cmd._args.length) { client.errorOut( new FirebaseError( - `Too many arguments. Run ${bold("firebase help " + this.name)} for usage instructions`, - { exit: 1 } - ) + `Too many arguments. Run ${clc.bold( + "firebase help " + this.name, + )} for usage instructions`, + { exit: 1 }, + ), ); return; } + const isEmulator = this.name.includes("emulator") || this.name === "serve"; + if (isEmulator) { + void trackEmulator("command_start", { command_name: this.name }); + } + runner(...args) - .then((result) => { + .then(async (result) => { if (getInheritedOption(options, "json")) { - console.log( - JSON.stringify( - { - status: "success", - result: result, - }, - null, - 2 - ) + await new Promise((resolve) => { + process.stdout.write( + JSON.stringify( + { + status: "success", + result: result, + }, + null, + 2, + ), + resolve, + ); + }); + } + const duration = Math.floor((process.uptime() - start) * 1000); + const trackSuccess = trackGA4("command_execution", { + command_name: this.name, + result: "success", + duration, + interactive: getInheritedOption(options, "nonInteractive") ? "false" : "true", + }); + if (!isEmulator) { + await withTimeout(5000, trackSuccess); + } else { + await withTimeout( + 5000, + Promise.all([ + trackSuccess, + trackEmulator("command_success", { + command_name: this.name, + duration, + }), + ]), ); } - const duration = new Date().getTime() - start; - track(this.name, "success", duration).then(() => process.exit()); + process.exit(); }) .catch(async (err) => { if (getInheritedOption(options, "json")) { - console.log( - JSON.stringify( + await new Promise((resolve) => { + process.stdout.write( + JSON.stringify( + { + status: "error", + error: err.message, + }, + null, + 2, + ), + resolve, + ); + }); + } + const duration = Math.floor((process.uptime() - start) * 1000); + await withTimeout( + 5000, + Promise.all([ + trackGA4( + "command_execution", { - status: "error", - error: err.message, + command_name: this.name, + result: "error", + interactive: getInheritedOption(options, "nonInteractive") ? "false" : "true", }, - null, - 2 - ) - ); - } - const duration = Date.now() - start; - const errorEvent = err.exit === 1 ? "Error (User)" : "Error (Unexpected)"; + duration, + ), + isEmulator + ? trackEmulator("command_error", { + command_name: this.name, + duration, + error_type: err.exit === 1 ? "user" : "unexpected", + }) + : Promise.resolve(), + ]), + ); - await Promise.all([track(this.name, "error", duration), track(errorEvent, "", duration)]); client.errorOut(err); }); }); @@ -224,7 +276,7 @@ export class Command { * @param options the command options object. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async prepare(options: any): Promise { + public async prepare(options: any): Promise { options = options || {}; options.project = getInheritedOption(options, "project"); @@ -253,7 +305,7 @@ export class Command { try { options.config = Config.load(options); - } catch (e) { + } catch (e: any) { options.configError = e; } @@ -325,7 +377,7 @@ export class Command { * @return an async function that executes the command. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - runner(): (...a: any[]) => Promise { + runner(): (...a: any[]) => Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any return async (...args: any[]) => { // Make sure the last argument is an object for options, add {} if none @@ -366,7 +418,10 @@ export function validateProjectId(project: string): void { if (PROJECT_ID_REGEX.test(project)) { return; } - track("Project ID Check", "invalid"); + trackGA4("error", { + error_type: "Error (User)", + details: "Invalid project ID", + }); const invalidMessage = "Invalid project id: " + clc.bold(project) + "."; if (project.toLowerCase() !== project) { // Attempt to be more helpful in case uppercase letters are used. diff --git a/src/commands/appdistribution-distribute.ts b/src/commands/appdistribution-distribute.ts index c371b5e5283..e67738b64e5 100644 --- a/src/commands/appdistribution-distribute.ts +++ b/src/commands/appdistribution-distribute.ts @@ -3,20 +3,26 @@ import * as fs from "fs-extra"; import { Command } from "../command"; import * as utils from "../utils"; import { requireAuth } from "../requireAuth"; +import { AppDistributionClient } from "../appdistribution/client"; import { AabInfo, IntegrationState, - AppDistributionClient, UploadReleaseResult, -} from "../appdistribution/client"; + TestDevice, +} from "../appdistribution/types"; import { FirebaseError } from "../error"; import { Distribution, DistributionFileType } from "../appdistribution/distribution"; import { ensureFileExists, getAppName, + getLoginCredential, + getTestDevices, getTestersOrGroups, } from "../appdistribution/options-parser-util"; +const TEST_MAX_POLLING_RETRIES = 40; +const TEST_POLLING_INTERVAL_MILLIS = 30_000; + function getReleaseNotes(releaseNotes: string, releaseNotesFile: string): string { if (releaseNotes) { // Un-escape new lines from argument string @@ -28,7 +34,7 @@ function getReleaseNotes(releaseNotes: string, releaseNotesFile: string): string return ""; } -module.exports = new Command("appdistribution:distribute ") +export const command = new Command("appdistribution:distribute ") .description("upload a release binary") .option("--app ", "the app id of your Firebase app") .option("--release-notes ", "release notes to include") @@ -36,12 +42,38 @@ module.exports = new Command("appdistribution:distribute ") .option("--testers ", "a comma separated list of tester emails to distribute to") .option( "--testers-file ", - "path to file with a comma separated list of tester emails to distribute to" + "path to file with a comma separated list of tester emails to distribute to", ) .option("--groups ", "a comma separated list of group aliases to distribute to") .option( "--groups-file ", - "path to file with a comma separated list of group aliases to distribute to" + "path to file with a comma separated list of group aliases to distribute to", + ) + .option( + "--test-devices ", + "semicolon-separated list of devices to run automated tests on, in the format 'model=,version=,locale=,orientation='. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.", + ) + .option( + "--test-devices-file ", + "path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=,version=,locale=,orientation='. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.", + ) + .option("--test-username ", "username for automatic login") + .option( + "--test-password ", + "password for automatic login. If using a real password, use --test-password-file instead to avoid putting sensitive info in history and logs.", + ) + .option("--test-password-file ", "path to file containing password for automatic login") + .option( + "--test-username-resource ", + "resource name for the username field for automatic login", + ) + .option( + "--test-password-resource ", + "resource name for the password field for automatic login", + ) + .option( + "--test-non-blocking", + "run automated tests without waiting for them to complete. Visit the Firebase console for the test results.", ) .before(requireAuth) .action(async (file: string, options: any) => { @@ -50,20 +82,28 @@ module.exports = new Command("appdistribution:distribute ") const releaseNotes = getReleaseNotes(options.releaseNotes, options.releaseNotesFile); const testers = getTestersOrGroups(options.testers, options.testersFile); const groups = getTestersOrGroups(options.groups, options.groupsFile); + const testDevices = getTestDevices(options.testDevices, options.testDevicesFile); + const loginCredential = getLoginCredential({ + username: options.testUsername, + password: options.testPassword, + passwordFile: options.testPasswordFile, + usernameResourceName: options.testUsernameResource, + passwordResourceName: options.testPasswordResource, + }); const requests = new AppDistributionClient(); let aabInfo: AabInfo | undefined; if (distribution.distributionFileType() === DistributionFileType.AAB) { try { aabInfo = await requests.getAabInfo(appName); - } catch (err) { + } catch (err: any) { if (err.status === 404) { throw new FirebaseError( `App Distribution could not find your app ${options.app}. ` + `Make sure to onboard your app by pressing the "Get started" ` + "button on the App Distribution page in the Firebase console: " + "https://console.firebase.google.com/project/_/appdistribution", - { exit: 1 } + { exit: 1 }, ); } throw new FirebaseError(`failed to determine AAB info. ${err.message}`, { exit: 1 }); @@ -82,17 +122,17 @@ module.exports = new Command("appdistribution:distribute ") } case IntegrationState.NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT: { throw new FirebaseError( - "App with matching package name does not exist in Google Play." + "App with matching package name does not exist in Google Play.", ); } case IntegrationState.PLAY_IAS_TERMS_NOT_ACCEPTED: { throw new FirebaseError( - "You must accept the Play Internal App Sharing (IAS) terms to upload AABs." + "You must accept the Play Internal App Sharing (IAS) terms to upload AABs.", ); } default: { throw new FirebaseError( - "App Distribution failed to process the AAB: " + aabInfo.integrationState + "App Distribution failed to process the AAB: " + aabInfo.integrationState, ); } } @@ -111,36 +151,41 @@ module.exports = new Command("appdistribution:distribute ") switch (uploadResponse.result) { case UploadReleaseResult.RELEASE_CREATED: utils.logSuccess( - `uploaded new release ${release.displayVersion} (${release.buildVersion}) successfully!` + `uploaded new release ${release.displayVersion} (${release.buildVersion}) successfully!`, ); break; case UploadReleaseResult.RELEASE_UPDATED: utils.logSuccess( - `uploaded update to existing release ${release.displayVersion} (${release.buildVersion}) successfully!` + `uploaded update to existing release ${release.displayVersion} (${release.buildVersion}) successfully!`, ); break; case UploadReleaseResult.RELEASE_UNMODIFIED: utils.logSuccess( - `re-uploaded already existing release ${release.displayVersion} (${release.buildVersion}) successfully!` + `re-uploaded already existing release ${release.displayVersion} (${release.buildVersion}) successfully!`, ); break; default: utils.logSuccess( - `uploaded release ${release.displayVersion} (${release.buildVersion}) successfully!` + `uploaded release ${release.displayVersion} (${release.buildVersion}) successfully!`, ); } + utils.logSuccess(`View this release in the Firebase console: ${release.firebaseConsoleUri}`); + utils.logSuccess(`Share this release with testers who have access: ${release.testingUri}`); + utils.logSuccess( + `Download the release binary (link expires in 1 hour): ${release.binaryDownloadUri}`, + ); releaseName = uploadResponse.release.name; - } catch (err) { + } catch (err: any) { if (err.status === 404) { throw new FirebaseError( `App Distribution could not find your app ${options.app}. ` + `Make sure to onboard your app by pressing the "Get started" ` + "button on the App Distribution page in the Firebase console: " + "https://console.firebase.google.com/project/_/appdistribution", - { exit: 1 } + { exit: 1 }, ); } - throw new FirebaseError(`failed to upload release. ${err.message}`, { exit: 1 }); + throw new FirebaseError(`Failed to upload release. ${err.message}`, { exit: 1 }); } // If this is an app bundle and the certificate was originally blank fetch the updated @@ -155,7 +200,7 @@ module.exports = new Command("appdistribution:distribute ") "signing key with API providers, such as Google Sign-In and Google Maps.\n" + `MD-1 certificate fingerprint: ${aabInfo.testCertificate.hashMd5}\n` + `SHA-1 certificate fingerprint: ${aabInfo.testCertificate.hashSha1}\n` + - `SHA-256 certificate fingerprint: ${aabInfo.testCertificate.hashSha256}` + `SHA-256 certificate fingerprint: ${aabInfo.testCertificate.hashSha256}`, ); } } @@ -163,4 +208,66 @@ module.exports = new Command("appdistribution:distribute ") // Add release notes and distribute to testers/groups await requests.updateReleaseNotes(releaseName, releaseNotes); await requests.distribute(releaseName, testers, groups); + + // Run automated tests + if (testDevices?.length) { + utils.logBullet("starting automated tests (note: this feature is in beta)"); + const releaseTest = await requests.createReleaseTest( + releaseName, + testDevices, + loginCredential, + ); + utils.logSuccess(`Release test created successfully`); + if (!options.testNonBlocking) { + await awaitTestResults(releaseTest.name!, requests); + } + } + }); + +async function awaitTestResults( + releaseTestName: string, + requests: AppDistributionClient, +): Promise { + for (let i = 0; i < TEST_MAX_POLLING_RETRIES; i++) { + utils.logBullet("the automated tests results are pending"); + await delay(TEST_POLLING_INTERVAL_MILLIS); + const releaseTest = await requests.getReleaseTest(releaseTestName); + if (releaseTest.deviceExecutions.every((e) => e.state === "PASSED")) { + utils.logSuccess("automated test(s) passed!"); + return; + } + for (const execution of releaseTest.deviceExecutions) { + switch (execution.state) { + case "PASSED": + case "IN_PROGRESS": + continue; + case "FAILED": + throw new FirebaseError( + `Automated test failed for ${deviceToString(execution.device)}: ${execution.failedReason}`, + { exit: 1 }, + ); + case "INCONCLUSIVE": + throw new FirebaseError( + `Automated test inconclusive for ${deviceToString(execution.device)}: ${execution.inconclusiveReason}`, + { exit: 1 }, + ); + default: + throw new FirebaseError( + `Unsupported automated test state for ${deviceToString(execution.device)}: ${execution.state}`, + { exit: 1 }, + ); + } + } + } + throw new FirebaseError("It took longer than expected to process your test, please try again.", { + exit: 1, }); +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function deviceToString(device: TestDevice): string { + return `${device.model} (${device.version}/${device.orientation}/${device.locale})`; +} diff --git a/src/commands/appdistribution-group-create.ts b/src/commands/appdistribution-group-create.ts new file mode 100644 index 00000000000..493d32af836 --- /dev/null +++ b/src/commands/appdistribution-group-create.ts @@ -0,0 +1,17 @@ +import { Command } from "../command"; +import * as utils from "../utils"; +import { requireAuth } from "../requireAuth"; +import { AppDistributionClient } from "../appdistribution/client"; +import { getProjectName } from "../appdistribution/options-parser-util"; + +export const command = new Command("appdistribution:group:create [alias]") + .description("create group in project") + .before(requireAuth) + .action(async (displayName: string, alias?: string, options?: any) => { + const projectName = await getProjectName(options); + const appDistroClient = new AppDistributionClient(); + utils.logBullet(`Creating group in project`); + const group = await appDistroClient.createGroup(projectName, displayName, alias); + alias = group.name.split("/").pop(); + utils.logSuccess(`Group '${group.displayName}' (alias: ${alias}) created successfully`); + }); diff --git a/src/commands/appdistribution-group-delete.ts b/src/commands/appdistribution-group-delete.ts new file mode 100644 index 00000000000..16651d711f8 --- /dev/null +++ b/src/commands/appdistribution-group-delete.ts @@ -0,0 +1,21 @@ +import { Command } from "../command"; +import * as utils from "../utils"; +import { requireAuth } from "../requireAuth"; +import { FirebaseError } from "../error"; +import { AppDistributionClient } from "../appdistribution/client"; +import { getProjectName } from "../appdistribution/options-parser-util"; + +export const command = new Command("appdistribution:group:delete ") + .description("delete group from a project") + .before(requireAuth) + .action(async (alias: string, options: any) => { + const projectName = await getProjectName(options); + const appDistroClient = new AppDistributionClient(); + try { + utils.logBullet(`Deleting group from project`); + await appDistroClient.deleteGroup(`${projectName}/groups/${alias}`); + } catch (err: any) { + throw new FirebaseError(`Failed to delete group ${err}`); + } + utils.logSuccess(`Group ${alias} has successfully been deleted`); + }); diff --git a/src/commands/appdistribution-testers-add.ts b/src/commands/appdistribution-testers-add.ts index 38c7b3ac4e6..81c63381606 100644 --- a/src/commands/appdistribution-testers-add.ts +++ b/src/commands/appdistribution-testers-add.ts @@ -1,14 +1,16 @@ import { Command } from "../command"; import * as utils from "../utils"; import { requireAuth } from "../requireAuth"; -import { FirebaseError } from "../error"; import { AppDistributionClient } from "../appdistribution/client"; import { getEmails, getProjectName } from "../appdistribution/options-parser-util"; -import { needProjectNumber } from "../projectUtils"; -module.exports = new Command("appdistribution:testers:add [emails...]") - .description("add testers to project") +export const command = new Command("appdistribution:testers:add [emails...]") + .description("add testers to project (and possibly group)") .option("--file ", "a path to a file containing a list of tester emails to be added") + .option( + "--group-alias ", + "if specified, the testers are also added to the group identified by this alias", + ) .before(requireAuth) .action(async (emails: string[], options?: any) => { const projectName = await getProjectName(options); @@ -16,4 +18,11 @@ module.exports = new Command("appdistribution:testers:add [emails...]") const emailsToAdd = getEmails(emails, options.file); utils.logBullet(`Adding ${emailsToAdd.length} testers to project`); await appDistroClient.addTesters(projectName, emailsToAdd); + if (options.groupAlias) { + utils.logBullet(`Adding ${emailsToAdd.length} testers to group`); + await appDistroClient.addTestersToGroup( + `${projectName}/groups/${options.groupAlias}`, + emailsToAdd, + ); + } }); diff --git a/src/commands/appdistribution-testers-remove.ts b/src/commands/appdistribution-testers-remove.ts index 7d9a01f155b..c3fb8c5b8bf 100644 --- a/src/commands/appdistribution-testers-remove.ts +++ b/src/commands/appdistribution-testers-remove.ts @@ -6,26 +6,38 @@ import { AppDistributionClient } from "../appdistribution/client"; import { getEmails, getProjectName } from "../appdistribution/options-parser-util"; import { logger } from "../logger"; -module.exports = new Command("appdistribution:testers:remove [emails...]") - .description("remove testers from a project") +export const command = new Command("appdistribution:testers:remove [emails...]") + .description("remove testers from a project (or group)") .option("--file ", "a path to a file containing a list of tester emails to be removed") + .option( + "--group-alias ", + "if specified, the testers are only removed from the group identified by this alias, but not the project", + ) .before(requireAuth) .action(async (emails: string[], options?: any) => { const projectName = await getProjectName(options); const appDistroClient = new AppDistributionClient(); const emailsArr = getEmails(emails, options.file); - let deleteResponse; - try { - utils.logBullet(`Deleting ${emailsArr.length} testers from project`); - deleteResponse = await appDistroClient.removeTesters(projectName, emailsArr); - } catch (err) { - throw new FirebaseError(`Failed to remove testers ${err}`); - } + if (options.groupAlias) { + utils.logBullet(`Removing ${emailsArr.length} testers from group`); + await appDistroClient.removeTestersFromGroup( + `${projectName}/groups/${options.groupAlias}`, + emailsArr, + ); + } else { + let deleteResponse; + try { + utils.logBullet(`Deleting ${emailsArr.length} testers from project`); + deleteResponse = await appDistroClient.removeTesters(projectName, emailsArr); + } catch (err: any) { + throw new FirebaseError(`Failed to remove testers ${err}`); + } - if (!deleteResponse.emails) { - utils.logSuccess(`Testers did not exist`); - return; + if (!deleteResponse.emails) { + utils.logSuccess(`Testers did not exist`); + return; + } + logger.debug(`Testers: ${deleteResponse.emails}, have been successfully deleted`); + utils.logSuccess(`${deleteResponse.emails.length} testers have successfully been deleted`); } - logger.debug(`Testers: ${deleteResponse.emails}, have been successfully deleted`); - utils.logSuccess(`${deleteResponse.emails.length} testers have successfully been deleted`); }); diff --git a/src/commands/apphosting-backends-create.ts b/src/commands/apphosting-backends-create.ts new file mode 100644 index 00000000000..d855d76e535 --- /dev/null +++ b/src/commands/apphosting-backends-create.ts @@ -0,0 +1,37 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import requireInteractive from "../requireInteractive"; +import { doSetup } from "../apphosting"; +import { ensureApiEnabled } from "../gcp/apphosting"; +import { APPHOSTING_TOS_ID } from "../gcp/firedata"; +import { requireTosAcceptance } from "../requireTosAcceptance"; + +export const command = new Command("apphosting:backends:create") + .description("create a Firebase App Hosting backend") + .option( + "-a, --app ", + "specify an existing Firebase web app's ID to associate your App Hosting backend with", + ) + .option("-l, --location ", "specify the location of the backend", "") + .option( + "-s, --service-account ", + "specify the service account used to run the server", + "", + ) + .before(ensureApiEnabled) + .before(requireInteractive) + .before(requireTosAcceptance(APPHOSTING_TOS_ID)) + .action(async (options: Options) => { + const projectId = needProjectId(options); + const webAppId = options.app; + const location = options.location; + const serviceAccount = options.serviceAccount; + + await doSetup( + projectId, + webAppId as string | null, + location as string | null, + serviceAccount as string | null, + ); + }); diff --git a/src/commands/apphosting-backends-delete.ts b/src/commands/apphosting-backends-delete.ts new file mode 100644 index 00000000000..d54e4f6192b --- /dev/null +++ b/src/commands/apphosting-backends-delete.ts @@ -0,0 +1,70 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { FirebaseError } from "../error"; +import { promptOnce } from "../prompt"; +import * as utils from "../utils"; +import * as apphosting from "../gcp/apphosting"; +import { printBackendsTable } from "./apphosting-backends-list"; +import { deleteBackendAndPoll, getBackendForAmbiguousLocation } from "../apphosting"; +import * as ora from "ora"; + +export const command = new Command("apphosting:backends:delete ") + .description("delete a Firebase App Hosting backend") + .option("-l, --location ", "specify the location of the backend", "-") + .withForce() + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + let location = options.location as string; + let backend: apphosting.Backend; + if (location === "-" || location === "") { + backend = await getBackendForAmbiguousLocation( + projectId, + backendId, + "Please select the location of the backend you'd like to delete:", + ); + location = apphosting.parseBackendName(backend.name).location; + } else { + backend = await getBackendForLocation(projectId, location, backendId); + } + + utils.logWarning("You are about to permanently delete this backend:"); + printBackendsTable([backend]); + + const confirmDeletion = await promptOnce( + { + type: "confirm", + name: "force", + default: false, + message: "Are you sure?", + }, + options, + ); + if (!confirmDeletion) { + return; + } + + const spinner = ora("Deleting backend...").start(); + try { + await deleteBackendAndPoll(projectId, location, backendId); + spinner.succeed(`Successfully deleted the backend: ${backendId}`); + } catch (err: any) { + spinner.stop(); + throw new FirebaseError(`Failed to delete backend: ${backendId}.`, { original: err }); + } + }); + +async function getBackendForLocation( + projectId: string, + location: string, + backendId: string, +): Promise { + try { + return await apphosting.getBackend(projectId, location, backendId); + } catch (err: any) { + throw new FirebaseError(`No backend named "${backendId}" found in ${location}.`, { + original: err, + }); + } +} diff --git a/src/commands/apphosting-backends-get.ts b/src/commands/apphosting-backends-get.ts new file mode 100644 index 00000000000..2f3832a5939 --- /dev/null +++ b/src/commands/apphosting-backends-get.ts @@ -0,0 +1,39 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { FirebaseError } from "../error"; +import { logWarning } from "../utils"; +import * as apphosting from "../gcp/apphosting"; +import { printBackendsTable } from "./apphosting-backends-list"; + +export const command = new Command("apphosting:backends:get ") + .description("print info about a Firebase App Hosting backend") + .option("-l, --location ", "backend location", "-") + .before(apphosting.ensureApiEnabled) + .action(async (backend: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + + let backendsList: apphosting.Backend[] = []; + try { + if (location !== "-") { + const backendInRegion = await apphosting.getBackend(projectId, location, backend); + backendsList.push(backendInRegion); + } else { + const resp = await apphosting.listBackends(projectId, "-"); + const allBackends = resp.backends || []; + backendsList = allBackends.filter((bkd) => bkd.name.split("/").pop() === backend); + } + } catch (err: any) { + throw new FirebaseError( + `Failed to get backend: ${backend}. Please check the parameters you have provided.`, + { original: err }, + ); + } + if (backendsList.length === 0) { + logWarning(`Backend "${backend}" not found`); + return; + } + printBackendsTable(backendsList); + return backendsList[0]; + }); diff --git a/src/commands/apphosting-backends-list.ts b/src/commands/apphosting-backends-list.ts new file mode 100644 index 00000000000..0b583e2d602 --- /dev/null +++ b/src/commands/apphosting-backends-list.ts @@ -0,0 +1,56 @@ +import { Command } from "../command"; +import { datetimeString } from "../utils"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { needProjectId } from "../projectUtils"; +import { Options } from "../options"; +import * as apphosting from "../gcp/apphosting"; + +const Table = require("cli-table"); +const TABLE_HEAD = ["Backend", "Repository", "URL", "Location", "Updated Date"]; + +export const command = new Command("apphosting:backends:list") + .description("list Firebase App Hosting backends") + .option("-l, --location ", "list backends in the specified location", "-") + .before(apphosting.ensureApiEnabled) + .action(async (options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + let backendRes: apphosting.ListBackendsResponse; + try { + backendRes = await apphosting.listBackends(projectId, location); + } catch (err: unknown) { + throw new FirebaseError( + `Unable to list backends present for project: ${projectId}. Please check the parameters you have provided.`, + { original: err as Error }, + ); + } + + const backends = backendRes.backends ?? []; + printBackendsTable(backends); + + return backends; + }); + +/** + * Prints a table given a list of backends + */ +export function printBackendsTable(backends: apphosting.Backend[]): void { + const table = new Table({ + head: TABLE_HEAD, + style: { head: ["green"] }, + }); + + for (const backend of backends) { + const { location, id } = apphosting.parseBackendName(backend.name); + table.push([ + id, + // sample repository value: "projects//locations/us-central1/connections//repositories/" + backend.codebase?.repository?.split("/").pop() ?? "", + backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri, + location, + datetimeString(new Date(backend.updateTime)), + ]); + } + logger.info(table.toString()); +} diff --git a/src/commands/apphosting-builds-create.ts b/src/commands/apphosting-builds-create.ts new file mode 100644 index 00000000000..2050917c92f --- /dev/null +++ b/src/commands/apphosting-builds-create.ts @@ -0,0 +1,33 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; + +export const command = new Command("apphosting:builds:create ") + .description("create a build for an App Hosting backend") + .option("-l, --location ", "specify the region of the backend", "us-central1") + .option("-i, --id ", "id of the build (defaults to autogenerating a random id)", "") + .option("-b, --branch ", "repository branch to deploy (defaults to 'main')", "main") + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const buildId = + (options.buildId as string) || + (await apphosting.getNextRolloutId(projectId, location, backendId)); + const branch = (options.branch as string | undefined) ?? "main"; + + const op = await apphosting.createBuild(projectId, location, backendId, buildId, { + source: { + codebase: { + branch, + }, + }, + }); + + logger.info(`Started a build for backend ${backendId} on branch ${branch}.`); + logger.info("Check status by running:"); + logger.info(`\tfirebase apphosting:builds:get ${backendId} ${buildId} --location ${location}`); + return op; + }); diff --git a/src/commands/apphosting-builds-get.ts b/src/commands/apphosting-builds-get.ts new file mode 100644 index 00000000000..1889b50029c --- /dev/null +++ b/src/commands/apphosting-builds-get.ts @@ -0,0 +1,17 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; + +export const command = new Command("apphosting:builds:get ") + .description("get a build for an App Hosting backend") + .option("-l, --location ", "specify the region of the backend", "us-central1") + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, buildId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const build = await apphosting.getBuild(projectId, location, backendId, buildId); + logger.info(JSON.stringify(build, null, 2)); + return build; + }); diff --git a/src/commands/apphosting-rollouts-create.ts b/src/commands/apphosting-rollouts-create.ts new file mode 100644 index 00000000000..5c3a64ee692 --- /dev/null +++ b/src/commands/apphosting-rollouts-create.ts @@ -0,0 +1,27 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; + +export const command = new Command("apphosting:rollouts:create ") + .description("create a rollout using a build for an App Hosting backend") + .option("-l, --location ", "specify the region of the backend", "us-central1") + .option("-i, --id ", "id of the rollout (defaults to autogenerating a random id)", "") + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, buildId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + // TODO: Should we just reuse the buildId? + const rolloutId = + (options.buildId as string) || + (await apphosting.getNextRolloutId(projectId, location, backendId)); + const build = `projects/${projectId}/backends/${backendId}/builds/${buildId}`; + const op = await apphosting.createRollout(projectId, location, backendId, rolloutId, { + build, + }); + logger.info(`Started a rollout for backend ${backendId} with build ${buildId}.`); + logger.info("Check status by running:"); + logger.info(`\tfirebase apphosting:rollouts:list --location ${location}`); + return op; + }); diff --git a/src/commands/apphosting-rollouts-list.ts b/src/commands/apphosting-rollouts-list.ts new file mode 100644 index 00000000000..5e3ac8c5350 --- /dev/null +++ b/src/commands/apphosting-rollouts-list.ts @@ -0,0 +1,26 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; + +export const command = new Command("apphosting:rollouts:list ") + .description("list rollouts of an App Hosting backend") + .option( + "-l, --location ", + "region of the rollouts (defaults to listing rollouts from all regions)", + "-", + ) + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const rollouts = await apphosting.listRollouts(projectId, location, backendId); + if (rollouts.unreachable) { + logger.error( + `WARNING: the following locations were unreachable: ${rollouts.unreachable.join(", ")}`, + ); + } + logger.info(JSON.stringify(rollouts.rollouts, null, 2)); + return rollouts; + }); diff --git a/src/commands/apphosting-secrets-access.ts b/src/commands/apphosting-secrets-access.ts new file mode 100644 index 00000000000..ecd5d539e7e --- /dev/null +++ b/src/commands/apphosting-secrets-access.ts @@ -0,0 +1,25 @@ +import { Command } from "../command"; +import { logger } from "../logger"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { accessSecretVersion } from "../gcp/secretManager"; +import { requireAuth } from "../requireAuth"; +import * as secretManager from "../gcp/secretManager"; +import { requirePermissions } from "../requirePermissions"; + +export const command = new Command("apphosting:secrets:access ") + .description( + "Access secret value given secret and its version. Defaults to accessing the latest version.", + ) + .before(requireAuth) + .before(secretManager.ensureApi) + .before(requirePermissions, ["secretmanager.versions.access"]) + .action(async (key: string, options: Options) => { + const projectId = needProjectId(options); + let [name, version] = key.split("@"); + if (!version) { + version = "latest"; + } + const value = await accessSecretVersion(projectId, name, version); + logger.info(value); + }); diff --git a/src/commands/apphosting-secrets-describe.ts b/src/commands/apphosting-secrets-describe.ts new file mode 100644 index 00000000000..7abafafa8ef --- /dev/null +++ b/src/commands/apphosting-secrets-describe.ts @@ -0,0 +1,30 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { logger } from "../logger"; +import { requireAuth } from "../requireAuth"; +import { listSecretVersions } from "../gcp/secretManager"; +import * as secretManager from "../gcp/secretManager"; +import { requirePermissions } from "../requirePermissions"; + +const Table = require("cli-table"); + +export const command = new Command("apphosting:secrets:describe ") + .description("Get metadata for secret and its versions.") + .before(requireAuth) + .before(secretManager.ensureApi) + .before(requirePermissions, ["secretmanager.secrets.get"]) + .action(async (secretName: string, options: Options) => { + const projectId = needProjectId(options); + const versions = await listSecretVersions(projectId, secretName); + + const table = new Table({ + head: ["Name", "Version", "Status", "Create Time"], + style: { head: ["yellow"] }, + }); + for (const version of versions) { + table.push([secretName, version.versionId, version.state, version.createTime]); + } + logger.info(table.toString()); + return { secrets: versions }; + }); diff --git a/src/commands/apphosting-secrets-grantaccess.ts b/src/commands/apphosting-secrets-grantaccess.ts new file mode 100644 index 00000000000..a99da896620 --- /dev/null +++ b/src/commands/apphosting-secrets-grantaccess.ts @@ -0,0 +1,58 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId, needProjectNumber } from "../projectUtils"; +import { FirebaseError } from "../error"; +import { requireAuth } from "../requireAuth"; +import * as secretManager from "../gcp/secretManager"; +import { requirePermissions } from "../requirePermissions"; +import * as apphosting from "../gcp/apphosting"; +import * as secrets from "../apphosting/secrets"; +import { getBackendForAmbiguousLocation } from "../apphosting"; + +export const command = new Command("apphosting:secrets:grantaccess ") + .description("grant service accounts permissions to the provided secret") + .option("-l, --location ", "backend location", "-") + .option("-b, --backend ", "backend name") + .before(requireAuth) + .before(secretManager.ensureApi) + .before(apphosting.ensureApiEnabled) + .before(requirePermissions, [ + "secretmanager.secrets.create", + "secretmanager.secrets.get", + "secretmanager.secrets.update", + "secretmanager.versions.add", + "secretmanager.secrets.getIamPolicy", + "secretmanager.secrets.setIamPolicy", + ]) + .action(async (secretName: string, options: Options) => { + const projectId = needProjectId(options); + const projectNumber = await needProjectNumber(options); + + if (!options.backend) { + throw new FirebaseError( + "Missing required flag --backend. See firebase apphosting:secrets:grantaccess --help for more info", + ); + } + + const exists = await secretManager.secretExists(projectId, secretName); + if (!exists) { + throw new FirebaseError(`Cannot find secret ${secretName}`); + } + + const backendId = options.backend as string; + const location = options.location as string; + let backend: apphosting.Backend; + if (location === "" || location === "-") { + backend = await getBackendForAmbiguousLocation( + projectId, + backendId, + "Please select the location of your backend:", + ); + } else { + backend = await apphosting.getBackend(projectId, location, backendId); + } + + const accounts = secrets.toMulti(secrets.serviceAccountsForBackend(projectNumber, backend)); + + await secrets.grantSecretAccess(projectId, projectNumber, secretName, accounts); + }); diff --git a/src/commands/apphosting-secrets-set.ts b/src/commands/apphosting-secrets-set.ts new file mode 100644 index 00000000000..390181afbb6 --- /dev/null +++ b/src/commands/apphosting-secrets-set.ts @@ -0,0 +1,78 @@ +import * as clc from "colorette"; + +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId, needProjectNumber } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import * as gcsm from "../gcp/secretManager"; +import * as apphosting from "../gcp/apphosting"; +import { requirePermissions } from "../requirePermissions"; +import * as secrets from "../apphosting/secrets"; +import * as dialogs from "../apphosting/secrets/dialogs"; +import * as config from "../apphosting/config"; +import * as utils from "../utils"; + +export const command = new Command("apphosting:secrets:set ") + .description("create or update a secret for use in Firebase App Hosting") + .option("-l, --location ", "optional location to retrict secret replication") + // TODO: What is the right --force behavior for granting access? Seems correct to grant permissions + // if there is only one set of accounts, but should maybe fail if there are more than one set of + // accounts for different backends? + .withForce("Automatically create a secret, grant permissions, and add to YAML.") + .before(requireAuth) + .before(gcsm.ensureApi) + .before(apphosting.ensureApiEnabled) + .before(requirePermissions, [ + "secretmanager.secrets.create", + "secretmanager.secrets.get", + "secretmanager.secrets.update", + "secretmanager.versions.add", + "secretmanager.secrets.getIamPolicy", + "secretmanager.secrets.setIamPolicy", + ]) + .option( + "--data-file ", + 'File path from which to read secret data. Set to "-" to read the secret data from stdin.', + ) + .action(async (secretName: string, options: Options) => { + const projectId = needProjectId(options); + const projectNumber = await needProjectNumber(options); + + const created = await secrets.upsertSecret(projectId, secretName, options.location as string); + if (created === null) { + return; + } else if (created) { + utils.logSuccess(`Created new secret projects/${projectId}/secrets/${secretName}`); + } + + const secretValue = await utils.readSecretValue( + `Enter a value for ${secretName}`, + options.dataFile as string | undefined, + ); + + const version = await gcsm.addVersion(projectId, secretName, secretValue); + utils.logSuccess(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`); + utils.logBullet( + `You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}\n`)}`, + ); + + // If the secret already exists, we want to exit once the new version is added + if (!created) { + return; + } + + const accounts = await dialogs.selectBackendServiceAccounts(projectNumber, projectId, options); + + // If we're not granting permissions, there's no point in adding to YAML either. + if (!accounts.buildServiceAccounts.length && !accounts.runServiceAccounts.length) { + utils.logWarning( + `To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantaccess")}`, + ); + + // TODO: For existing secrets, enter the grantSecretAccess dialog only when the necessary permissions don't exist. + } else { + await secrets.grantSecretAccess(projectId, projectNumber, secretName, accounts); + } + + await config.maybeAddSecretToYaml(secretName); + }); diff --git a/src/commands/apps-android-sha-create.ts b/src/commands/apps-android-sha-create.ts index f42a47efe0a..5103374f4f3 100644 --- a/src/commands/apps-android-sha-create.ts +++ b/src/commands/apps-android-sha-create.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { Command } from "../command"; import { needProjectId } from "../projectUtils"; @@ -9,12 +9,12 @@ import { promiseWithSpinner } from "../utils"; function getCertHashType(shaHash: string): string { shaHash = shaHash.replace(/:/g, ""); const shaHashCount = shaHash.length; - if (shaHashCount == 40) return ShaCertificateType.SHA_1.toString(); - if (shaHashCount == 64) return ShaCertificateType.SHA_256.toString(); + if (shaHashCount === 40) return ShaCertificateType.SHA_1.toString(); + if (shaHashCount === 64) return ShaCertificateType.SHA_256.toString(); return ShaCertificateType.SHA_CERTIFICATE_TYPE_UNSPECIFIED.toString(); } -module.exports = new Command("apps:android:sha:create ") +export const command = new Command("apps:android:sha:create ") .description("add a SHA certificate hash for a given app id.") .before(requireAuth) .action( @@ -28,10 +28,10 @@ module.exports = new Command("apps:android:sha:create ") certType: getCertHashType(shaHash), }), `Creating Android SHA certificate ${clc.bold( - options.shaHash - )}with Android app Id ${clc.bold(appId)}` + options.shaHash, + )}with Android app Id ${clc.bold(appId)}`, ); return shaCertificate; - } + }, ); diff --git a/src/commands/apps-android-sha-delete.ts b/src/commands/apps-android-sha-delete.ts index 94197053691..209d0376651 100644 --- a/src/commands/apps-android-sha-delete.ts +++ b/src/commands/apps-android-sha-delete.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { Command } from "../command"; import { needProjectId } from "../projectUtils"; @@ -6,18 +6,16 @@ import { deleteAppAndroidSha } from "../management/apps"; import { requireAuth } from "../requireAuth"; import { promiseWithSpinner } from "../utils"; -module.exports = new Command("apps:android:sha:delete ") +export const command = new Command("apps:android:sha:delete ") .description("delete a SHA certificate hash for a given app id.") .before(requireAuth) - .action( - async (appId: string = "", shaId: string = "", options: any): Promise => { - const projectId = needProjectId(options); + .action(async (appId: string = "", shaId: string = "", options: any): Promise => { + const projectId = needProjectId(options); - await promiseWithSpinner( - async () => await deleteAppAndroidSha(projectId, appId, shaId), - `Deleting Android SHA certificate hash with SHA id ${clc.bold( - shaId - )} and Android app Id ${clc.bold(appId)}` - ); - } - ); + await promiseWithSpinner( + async () => await deleteAppAndroidSha(projectId, appId, shaId), + `Deleting Android SHA certificate hash with SHA id ${clc.bold( + shaId, + )} and Android app Id ${clc.bold(appId)}`, + ); + }); diff --git a/src/commands/apps-android-sha-list.ts b/src/commands/apps-android-sha-list.ts index 763ca784724..bd24b595c38 100644 --- a/src/commands/apps-android-sha-list.ts +++ b/src/commands/apps-android-sha-list.ts @@ -1,4 +1,4 @@ -import Table = require("cli-table"); +const Table = require("cli-table"); import { Command } from "../command"; import { needProjectId } from "../projectUtils"; @@ -36,20 +36,18 @@ function logCertificatesCount(count: number = 0): void { logger.info(`${count} SHA hash(es) total.`); } -module.exports = new Command("apps:android:sha:list ") +export const command = new Command("apps:android:sha:list ") .description("list the SHA certificate hashes for a given app id. ") .before(requireAuth) - .action( - async (appId: string = "", options: any): Promise => { - const projectId = needProjectId(options); + .action(async (appId: string = "", options: any): Promise => { + const projectId = needProjectId(options); - const shaCertificates = await promiseWithSpinner( - async () => await listAppAndroidSha(projectId, appId), - "Preparing the list of your Firebase Android app SHA certificate hashes" - ); + const shaCertificates = await promiseWithSpinner( + async () => await listAppAndroidSha(projectId, appId), + "Preparing the list of your Firebase Android app SHA certificate hashes", + ); - logCertificatesList(shaCertificates); - logCertificatesCount(shaCertificates.length); - return shaCertificates; - } - ); + logCertificatesList(shaCertificates); + logCertificatesCount(shaCertificates.length); + return shaCertificates; + }); diff --git a/src/commands/apps-create.ts b/src/commands/apps-create.ts index c4c19c7e824..fa08038dcc7 100644 --- a/src/commands/apps-create.ts +++ b/src/commands/apps-create.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as ora from "ora"; import { Command } from "../command"; @@ -47,7 +47,7 @@ interface CreateWebAppOptions extends CreateFirebaseAppOptions { function logPostAppCreationInformation( appMetadata: IosAppMetadata | AndroidAppMetadata | WebAppMetadata, - appPlatform: AppPlatform + appPlatform: AppPlatform, ): void { logger.info(""); logger.info(`🎉🎉🎉 Your Firebase ${appPlatform} App is ready! 🎉🎉🎉`); @@ -103,14 +103,14 @@ async function initiateIosAppCreation(options: CreateIosAppOptions): Promise { if (!options.nonInteractive) { await prompt(options, [ @@ -135,7 +135,7 @@ async function initiateAndroidAppCreation( }); spinner.succeed(); return appData; - } catch (err) { + } catch (err: any) { spinner.fail(); throw err; } @@ -153,15 +153,15 @@ async function initiateWebAppCreation(options: CreateWebAppOptions): Promise", "required package name for the Android app") .option("-b, --bundle-id ", "required bundle id for the iOS app") @@ -171,7 +171,7 @@ module.exports = new Command("apps:create [platform] [displayName]") async ( platform: string = "", displayName: string | undefined, - options: any + options: any, ): Promise => { const projectId = needProjectId(options); @@ -211,5 +211,5 @@ module.exports = new Command("apps:create [platform] [displayName]") logPostAppCreationInformation(appData, appPlatform); return appData; - } + }, ); diff --git a/src/commands/apps-list.ts b/src/commands/apps-list.ts index 30335ad762d..bb75ef23a3c 100644 --- a/src/commands/apps-list.ts +++ b/src/commands/apps-list.ts @@ -1,6 +1,6 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as ora from "ora"; -import Table = require("cli-table"); +const Table = require("cli-table"); import { Command } from "../command"; import { needProjectId } from "../projectUtils"; @@ -32,32 +32,30 @@ function logAppCount(count: number = 0): void { logger.info(`${count} app(s) total.`); } -module.exports = new Command("apps:list [platform]") +export const command = new Command("apps:list [platform]") .description( "list the registered apps of a Firebase project. " + - "Optionally filter apps by [platform]: IOS, ANDROID or WEB (case insensitive)" + "Optionally filter apps by [platform]: IOS, ANDROID or WEB (case insensitive)", ) .before(requireAuth) - .action( - async (platform: string | undefined, options: any): Promise => { - const projectId = needProjectId(options); - const appPlatform = getAppPlatform(platform || ""); - - let apps; - const spinner = ora( - "Preparing the list of your Firebase " + - `${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps` - ).start(); - try { - apps = await listFirebaseApps(projectId, appPlatform); - } catch (err) { - spinner.fail(); - throw err; - } - - spinner.succeed(); - logAppsList(apps); - logAppCount(apps.length); - return apps; + .action(async (platform: string | undefined, options: any): Promise => { + const projectId = needProjectId(options); + const appPlatform = getAppPlatform(platform || ""); + + let apps; + const spinner = ora( + "Preparing the list of your Firebase " + + `${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps`, + ).start(); + try { + apps = await listFirebaseApps(projectId, appPlatform); + } catch (err: any) { + spinner.fail(); + throw err; } - ); + + spinner.succeed(); + logAppsList(apps); + logAppCount(apps.length); + return apps; + }); diff --git a/src/commands/apps-sdkconfig.ts b/src/commands/apps-sdkconfig.ts index ff429af1e4f..4abaaf3b2f4 100644 --- a/src/commands/apps-sdkconfig.ts +++ b/src/commands/apps-sdkconfig.ts @@ -17,17 +17,22 @@ import { FirebaseError } from "../error"; import { requireAuth } from "../requireAuth"; import { logger } from "../logger"; import { promptOnce } from "../prompt"; +import { Options } from "../options"; -async function selectAppInteractively( - apps: AppMetadata[], - appPlatform: AppPlatform -): Promise { - if (apps.length === 0) { +function checkForApps(apps: AppMetadata[], appPlatform: AppPlatform): void { + if (!apps.length) { throw new FirebaseError( `There are no ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps ` + - "associated with this Firebase project" + "associated with this Firebase project", ); } +} + +async function selectAppInteractively( + apps: AppMetadata[], + appPlatform: AppPlatform, +): Promise { + checkForApps(apps, appPlatform); // eslint-disable-next-line @typescript-eslint/no-explicit-any const choices = apps.map((app: any) => { @@ -48,85 +53,85 @@ async function selectAppInteractively( }); } -module.exports = new Command("apps:sdkconfig [platform] [appId]") +export const command = new Command("apps:sdkconfig [platform] [appId]") .description( "print the Google Services config of a Firebase app. " + - "[platform] can be IOS, ANDROID or WEB (case insensitive)" + "[platform] can be IOS, ANDROID or WEB (case insensitive)", ) .option("-o, --out [file]", "(optional) write config output to a file") .before(requireAuth) - .action( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (platform = "", appId = "", options: any): Promise => { - let appPlatform = getAppPlatform(platform); + .action(async (platform = "", appId = "", options: Options): Promise => { + let appPlatform = getAppPlatform(platform); - if (!appId) { - let projectId = needProjectId(options); - if (options.nonInteractive && !projectId) { - throw new FirebaseError("Must supply app and project ids in non-interactive mode."); - } else if (!projectId) { - const result = await getOrPromptProject(options); - projectId = result.projectId; - } - - const apps = await listFirebaseApps(projectId, appPlatform); - // if there's only one app, we don't need to prompt interactively - if (apps.length === 1) { - appId = apps[0].appId; - appPlatform = apps[0].platform; - } else if (options.nonInteractive) { - throw new FirebaseError( - `Project ${projectId} has multiple apps, must specify an app id.` - ); - } else { - const appMetadata: AppMetadata = await selectAppInteractively(apps, appPlatform); - appId = appMetadata.appId; - appPlatform = appMetadata.platform; - } + if (!appId) { + let projectId = needProjectId(options); + if (options.nonInteractive && !projectId) { + throw new FirebaseError("Must supply app and project ids in non-interactive mode."); + } else if (!projectId) { + const result = await getOrPromptProject(options); + projectId = result.projectId; } - let configData; - const spinner = ora( - `Downloading configuration data of your Firebase ${appPlatform} app` - ).start(); - try { - configData = await getAppConfig(appId, appPlatform); - } catch (err) { - spinner.fail(); - throw err; + const apps = await listFirebaseApps(projectId, appPlatform); + // Fail out early if there's no apps. + checkForApps(apps, appPlatform); + // if there's only one app, we don't need to prompt interactively + if (apps.length === 1) { + // If there's only one, use it. + appId = apps[0].appId; + appPlatform = apps[0].platform; + } else if (options.nonInteractive) { + // If there's > 1 and we're non-interactive, fail. + throw new FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`); + } else { + // > 1, ask what the user wants. + const appMetadata: AppMetadata = await selectAppInteractively(apps, appPlatform); + appId = appMetadata.appId; + appPlatform = appMetadata.platform; } - spinner.succeed(); + } - const fileInfo = getAppConfigFile(configData, appPlatform); - if (appPlatform == AppPlatform.WEB) { - fileInfo.sdkConfig = configData; - } + let configData; + const spinner = ora( + `Downloading configuration data of your Firebase ${appPlatform} app`, + ).start(); + try { + configData = await getAppConfig(appId, appPlatform); + } catch (err: any) { + spinner.fail(); + throw err; + } + spinner.succeed(); - if (options.out === undefined) { - logger.info(fileInfo.fileContents); - return fileInfo; - } + const fileInfo = getAppConfigFile(configData, appPlatform); + if (appPlatform === AppPlatform.WEB) { + fileInfo.sdkConfig = configData; + } + + if (options.out === undefined) { + logger.info(fileInfo.fileContents); + return fileInfo; + } - const shouldUseDefaultFilename = options.out === true || options.out === ""; - const filename = shouldUseDefaultFilename ? configData.fileName : options.out; - if (fs.existsSync(filename)) { - if (options.nonInteractive) { - throw new FirebaseError(`${filename} already exists`); - } - const overwrite = await promptOnce({ - type: "confirm", - default: false, - message: `${filename} already exists. Do you want to overwrite?`, - }); + const shouldUseDefaultFilename = options.out === true || options.out === ""; + const filename = shouldUseDefaultFilename ? configData.fileName : options.out; + if (fs.existsSync(filename)) { + if (options.nonInteractive) { + throw new FirebaseError(`${filename} already exists`); + } + const overwrite = await promptOnce({ + type: "confirm", + default: false, + message: `${filename} already exists. Do you want to overwrite?`, + }); - if (!overwrite) { - return configData; - } + if (!overwrite) { + return configData; } + } - fs.writeFileSync(filename, fileInfo.fileContents); - logger.info(`App configuration is written in ${filename}`); + fs.writeFileSync(filename, fileInfo.fileContents); + logger.info(`App configuration is written in ${filename}`); - return configData; - } - ); + return configData; + }); diff --git a/src/commands/auth-export.js b/src/commands/auth-export.js deleted file mode 100644 index 4f6c6aa5168..00000000000 --- a/src/commands/auth-export.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict"; - -var clc = require("cli-color"); -var fs = require("fs"); -var os = require("os"); - -var { Command } = require("../command"); -var accountExporter = require("../accountExporter"); -var needProjectId = require("../projectUtils").needProjectId; -const { logger } = require("../logger"); -var { requirePermissions } = require("../requirePermissions"); - -var MAX_BATCH_SIZE = 1000; - -var validateOptions = accountExporter.validateOptions; -var serialExportUsers = accountExporter.serialExportUsers; - -module.exports = new Command("auth:export [dataFile]") - .description("Export accounts from your Firebase project into a data file") - .option( - "--format ", - "Format of exported data (csv, json). Ignored if [dataFile] has format extension." - ) - .before(requirePermissions, ["firebaseauth.users.get"]) - .action(function (dataFile, options) { - var projectId = needProjectId(options); - var checkRes = validateOptions(options, dataFile); - if (!checkRes.format) { - return checkRes; - } - var exportOptions = checkRes; - var writeStream = fs.createWriteStream(dataFile); - if (exportOptions.format === "json") { - writeStream.write('{"users": [' + os.EOL); - } - exportOptions.writeStream = writeStream; - exportOptions.batchSize = MAX_BATCH_SIZE; - logger.info("Exporting accounts to " + clc.bold(dataFile)); - return serialExportUsers(projectId, exportOptions).then(function () { - if (exportOptions.format === "json") { - writeStream.write("]}"); - } - writeStream.end(); - // Ensure process ends only when all data have been flushed - // to the output file - return new Promise(function (resolve, reject) { - writeStream.on("finish", resolve); - writeStream.on("close", resolve); - writeStream.on("error", reject); - }); - }); - }); diff --git a/src/commands/auth-export.ts b/src/commands/auth-export.ts new file mode 100644 index 00000000000..1de847d6b05 --- /dev/null +++ b/src/commands/auth-export.ts @@ -0,0 +1,55 @@ +import * as clc from "colorette"; +import * as fs from "fs"; +import * as os from "os"; + +import { Command } from "../command"; +import { logger } from "../logger"; +import { needProjectId } from "../projectUtils"; +import { requirePermissions } from "../requirePermissions"; +import { validateOptions, serialExportUsers } from "../accountExporter"; + +const MAX_BATCH_SIZE = 1000; + +interface exportOptions { + format: string; + writeStream: fs.WriteStream; + batchSize: number; +} + +export const command = new Command("auth:export [dataFile]") + .description("Export accounts from your Firebase project into a data file") + .option( + "--format ", + "Format of exported data (csv, json). Ignored if has format extension.", + ) + .before(requirePermissions, ["firebaseauth.users.get"]) + .action((dataFile, options) => { + const projectId = needProjectId(options); + const checkRes = validateOptions(options, dataFile); + if (!checkRes.format) { + return checkRes; + } + const writeStream = fs.createWriteStream(dataFile); + if (checkRes.format === "json") { + writeStream.write('{"users": [' + os.EOL); + } + const exportOptions: exportOptions = { + format: checkRes.format, + writeStream, + batchSize: MAX_BATCH_SIZE, + }; + logger.info("Exporting accounts to " + clc.bold(dataFile)); + return serialExportUsers(projectId, exportOptions).then(() => { + if (exportOptions.format === "json") { + writeStream.write("]}"); + } + writeStream.end(); + // Ensure process ends only when all data have been flushed + // to the output file + return new Promise((resolve, reject) => { + writeStream.on("finish", resolve); + writeStream.on("close", resolve); + writeStream.on("error", reject); + }); + }); + }); diff --git a/src/commands/auth-import.js b/src/commands/auth-import.js deleted file mode 100644 index 5f959fc61d0..00000000000 --- a/src/commands/auth-import.js +++ /dev/null @@ -1,138 +0,0 @@ -"use strict"; - -var csv = require("csv-streamify"); -var clc = require("cli-color"); -var fs = require("fs"); -var jsonStream = require("JSONStream"); -var _ = require("lodash"); - -var { Command } = require("../command"); -var accountImporter = require("../accountImporter"); -var needProjectId = require("../projectUtils").needProjectId; -const { logger } = require("../logger"); -var { requirePermissions } = require("../requirePermissions"); -var utils = require("../utils"); - -var MAX_BATCH_SIZE = 1000; -var validateOptions = accountImporter.validateOptions; -var validateUserJson = accountImporter.validateUserJson; -var transArrayToUser = accountImporter.transArrayToUser; -var serialImportUsers = accountImporter.serialImportUsers; - -module.exports = new Command("auth:import [dataFile]") - .description("import users into your Firebase project from a data file(.csv or .json)") - .option( - "--hash-algo ", - "specify the hash algorithm used in password for these accounts" - ) - .option("--hash-key ", "specify the key used in hash algorithm") - .option( - "--salt-separator ", - "specify the salt separator which will be appended to salt when verifying password. only used by SCRYPT now." - ) - .option("--rounds ", "specify how many rounds for hash calculation.") - .option( - "--mem-cost ", - "specify the memory cost for firebase scrypt, or cpu/memory cost for standard scrypt" - ) - .option("--parallelization ", "specify the parallelization for standard scrypt.") - .option("--block-size ", "specify the block size (normally is 8) for standard scrypt.") - .option("--dk-len ", "specify derived key length for standard scrypt.") - .option( - "--hash-input-order ", - "specify the order of password and salt. Possible values are SALT_FIRST and PASSWORD_FIRST. " + - "MD5, SHA1, SHA256, SHA512, HMAC_MD5, HMAC_SHA1, HMAC_SHA256, HMAC_SHA512 support this flag." - ) - .before(requirePermissions, ["firebaseauth.users.create", "firebaseauth.users.update"]) - .action(function (dataFile, options) { - var projectId = needProjectId(options); - var checkRes = validateOptions(options); - if (!checkRes.valid) { - return checkRes; - } - var hashOptions = checkRes; - - if (!_.endsWith(dataFile, ".csv") && !_.endsWith(dataFile, ".json")) { - return utils.reject("Data file must end with .csv or .json", { exit: 1 }); - } - var stats = fs.statSync(dataFile); - var fileSizeInBytes = stats.size; - logger.info("Processing " + clc.bold(dataFile) + " (" + fileSizeInBytes + " bytes)"); - - var inStream = fs.createReadStream(dataFile); - var batches = []; - var currentBatch = []; - var counter = 0; - return new Promise(function (resolve, reject) { - var parser; - if (dataFile.endsWith(".csv")) { - parser = csv({ objectMode: true }); - parser - .on("data", function (line) { - counter++; - var user = transArrayToUser( - line.map(function (str) { - // Ignore starting '|'' and trailing '|'' - var newStr = str.trim().replace(/^["|'](.*)["|']$/, "$1"); - return newStr === "" ? undefined : newStr; - }) - ); - if (user.error) { - return reject( - "Line " + counter + " (" + line + ") has invalid data format: " + user.error - ); - } - currentBatch.push(user); - if (currentBatch.length === MAX_BATCH_SIZE) { - batches.push(currentBatch); - currentBatch = []; - } - }) - .on("end", function () { - if (currentBatch.length) { - batches.push(currentBatch); - } - return resolve(batches); - }); - inStream.pipe(parser); - } else { - parser = jsonStream.parse(["users", { emitKey: true }]); - parser - .on("data", function (pair) { - counter++; - var res = validateUserJson(pair.value); - if (res.error) { - return reject(res.error); - } - currentBatch.push(pair.value); - if (currentBatch.length === MAX_BATCH_SIZE) { - batches.push(currentBatch); - currentBatch = []; - } - }) - .on("end", function () { - if (currentBatch.length) { - batches.push(currentBatch); - } - return resolve(batches); - }); - inStream.pipe(parser); - } - }).then( - function (userListArr) { - logger.debug( - "Preparing to import", - counter, - "user records in", - userListArr.length, - "batches." - ); - if (userListArr.length) { - return serialImportUsers(projectId, hashOptions, userListArr, 0); - } - }, - function (error) { - return utils.reject(error, { exit: 1 }); - } - ); - }); diff --git a/src/commands/auth-import.ts b/src/commands/auth-import.ts new file mode 100644 index 00000000000..99011af78be --- /dev/null +++ b/src/commands/auth-import.ts @@ -0,0 +1,140 @@ +import { parse } from "csv-parse"; +import * as Chain from "stream-chain"; +import * as clc from "colorette"; +import * as fs from "fs-extra"; +import * as Pick from "stream-json/filters/Pick"; +import * as StreamArray from "stream-json/streamers/StreamArray"; + +import { Command } from "../command"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { needProjectId } from "../projectUtils"; +import { Options } from "../options"; +import { requirePermissions } from "../requirePermissions"; +import { + serialImportUsers, + transArrayToUser, + validateOptions, + validateUserJson, +} from "../accountImporter"; + +const MAX_BATCH_SIZE = 1000; + +export const command = new Command("auth:import [dataFile]") + .description("import users into your Firebase project from a data file(.csv or .json)") + .option( + "--hash-algo ", + "specify the hash algorithm used in password for these accounts", + ) + .option("--hash-key ", "specify the key used in hash algorithm") + .option( + "--salt-separator ", + "specify the salt separator which will be appended to salt when verifying password. only used by SCRYPT now.", + ) + .option("--rounds ", "specify how many rounds for hash calculation.") + .option( + "--mem-cost ", + "specify the memory cost for firebase scrypt, or cpu/memory cost for standard scrypt", + ) + .option("--parallelization ", "specify the parallelization for standard scrypt.") + .option("--block-size ", "specify the block size (normally is 8) for standard scrypt.") + .option("--dk-len ", "specify derived key length for standard scrypt.") + .option( + "--hash-input-order ", + "specify the order of password and salt. Possible values are SALT_FIRST and PASSWORD_FIRST. " + + "MD5, SHA1, SHA256, SHA512, HMAC_MD5, HMAC_SHA1, HMAC_SHA256, HMAC_SHA512 support this flag.", + ) + .before(requirePermissions, ["firebaseauth.users.create", "firebaseauth.users.update"]) + .action(async (dataFile: string, options: Options) => { + const projectId = needProjectId(options); + const checkRes = validateOptions(options); + if (!checkRes.valid) { + return checkRes; + } + const hashOptions = checkRes; + + if (!dataFile.endsWith(".csv") && !dataFile.endsWith(".json")) { + throw new FirebaseError("Data file must end with .csv or .json"); + } + const stats = await fs.stat(dataFile); + const fileSizeInBytes = stats.size; + logger.info(`Processing ${clc.bold(dataFile)} (${fileSizeInBytes} bytes)`); + + const batches: any[] = []; + let currentBatch: any[] = []; + let counter = 0; + let userListArr: any[] = []; + const inStream = fs.createReadStream(dataFile); + if (dataFile.endsWith(".csv")) { + userListArr = await new Promise((resolve, reject) => { + const parser = parse(); + parser + .on("readable", () => { + let record: string[] = []; + while ((record = parser.read()) !== null) { + counter++; + const trimmed = record.map((s) => { + const str = s.trim().replace(/^["|'](.*)["|']$/, "$1"); + return str === "" ? undefined : str; + }); + const user = transArrayToUser(trimmed); + // TODO: Remove this casst once user can have an error. + const err = (user as any).error; + if (err) { + return reject( + new FirebaseError( + `Line ${counter} (${record.join(",")}) has invalid data format: ${err}`, + ), + ); + } + currentBatch.push(user); + if (currentBatch.length === MAX_BATCH_SIZE) { + batches.push(currentBatch); + currentBatch = []; + } + } + }) + .on("end", () => { + if (currentBatch.length) { + batches.push(currentBatch); + } + resolve(batches); + }); + inStream.pipe(parser); + }); + } else { + userListArr = await new Promise((resolve, reject) => { + const pipeline = new Chain([ + Pick.withParser({ filter: /^users$/ }), + StreamArray.streamArray(), + ({ value }) => { + counter++; + const user = validateUserJson(value); + // TODO: Remove this casst once user can have an error. + const err = (user as any).error; + if (err) { + throw new FirebaseError(`Validation Error: ${err}`); + } + currentBatch.push(value); + if (currentBatch.length === MAX_BATCH_SIZE) { + batches.push(currentBatch); + currentBatch = []; + } + }, + ]); + pipeline.once("error", reject); + pipeline.on("finish", () => { + if (currentBatch.length) { + batches.push(currentBatch); + } + resolve(batches); + }); + inStream.pipe(pipeline); + }); + } + + logger.debug(`Preparing to import ${counter} user records in ${userListArr.length} batches.`); + if (userListArr.length) { + return serialImportUsers(projectId, hashOptions, userListArr, 0); + } + }); diff --git a/src/commands/crashlytics-mappingfile-generateid.ts b/src/commands/crashlytics-mappingfile-generateid.ts new file mode 100644 index 00000000000..615051e403a --- /dev/null +++ b/src/commands/crashlytics-mappingfile-generateid.ts @@ -0,0 +1,44 @@ +import { Command } from "../command"; +import * as utils from "../utils"; + +import { fetchBuildtoolsJar, runBuildtoolsCommand } from "../crashlytics/buildToolsJarHelper"; +import { Options } from "../options"; +import { FirebaseError } from "../error"; + +interface CommandOptions extends Options { + resourceFile: string; +} + +interface JarOptions { + resourceFilePath: string; +} + +export const command = new Command("crashlytics:mappingfile:generateid") + .description( + "generate a mapping file id and write it to an Android resource file, which will be built into the app", + ) + .option( + "--resource-file ", + "path to the Android resource XML file that will be created or updated.", + ) + .action(async (options: CommandOptions) => { + const debug = !!options.debug; + // Input errors will be caught in the buildtools jar. + const resourceFilePath = options.resourceFile; + if (!resourceFilePath) { + throw new FirebaseError( + "set --resource-file to an Android resource file path, e.g. app/src/main/res/values/crashlytics.xml", + ); + } + const jarFile = await fetchBuildtoolsJar(); + const jarOptions: JarOptions = { resourceFilePath }; + + utils.logBullet(`Updating resource file: ${resourceFilePath}`); + const generateIdArgs = buildArgs(jarOptions); + runBuildtoolsCommand(jarFile, generateIdArgs, debug); + utils.logBullet("Successfully updated mapping file id"); + }); + +function buildArgs(options: JarOptions): string[] { + return ["-injectMappingFileIdIntoResource", options.resourceFilePath, "-verbose"]; +} diff --git a/src/commands/crashlytics-mappingfile-upload.ts b/src/commands/crashlytics-mappingfile-upload.ts new file mode 100644 index 00000000000..2ecc24555d5 --- /dev/null +++ b/src/commands/crashlytics-mappingfile-upload.ts @@ -0,0 +1,72 @@ +import { Command } from "../command"; +import { FirebaseError } from "../error"; +import * as utils from "../utils"; + +import { fetchBuildtoolsJar, runBuildtoolsCommand } from "../crashlytics/buildToolsJarHelper"; +import { Options } from "../options"; + +interface CommandOptions extends Options { + app?: string; + mappingFile?: string; + resourceFile?: string; +} + +interface JarOptions { + app: string; + mappingFilePath: string; + resourceFilePath: string; +} + +export const command = new Command("crashlytics:mappingfile:upload ") + .description("upload a ProGuard/R8-compatible mapping file to deobfuscate stack traces") + .option("--app ", "the app id of your Firebase app") + .option( + "--resource-file ", + "path to the Android resource XML file that includes the mapping file id", + ) + .action(async (mappingFile: string, options: CommandOptions) => { + const app = getGoogleAppID(options); + const debug = !!options.debug; + if (!mappingFile) { + throw new FirebaseError( + "set `--mapping-file ` to a valid mapping file path, e.g. app/build/outputs/mapping.txt", + ); + } + const mappingFilePath = mappingFile; + + const resourceFilePath = options.resourceFile; + if (!resourceFilePath) { + throw new FirebaseError( + "set --resource-file to a valid Android resource file path, e.g. app/main/res/values/strings.xml", + ); + } + + const jarFile = await fetchBuildtoolsJar(); + const jarOptions: JarOptions = { app, mappingFilePath, resourceFilePath }; + + utils.logBullet(`Uploading mapping file: ${mappingFilePath}`); + const uploadArgs = buildArgs(jarOptions); + runBuildtoolsCommand(jarFile, uploadArgs, debug); + utils.logBullet("Successfully uploaded mapping file"); + }); + +function getGoogleAppID(options: CommandOptions): string { + if (!options.app) { + throw new FirebaseError( + "set --app to a valid Firebase application id, e.g. 1:00000000:android:0000000", + ); + } + return options.app; +} + +function buildArgs(options: JarOptions): string[] { + return [ + "-uploadMappingFile", + options.mappingFilePath, + "-resourceFile", + options.resourceFilePath, + "-googleAppId", + options.app, + "-verbose", + ]; +} diff --git a/src/commands/crashlytics-symbols-upload.ts b/src/commands/crashlytics-symbols-upload.ts index a3d4755b3c1..e2123a393d9 100644 --- a/src/commands/crashlytics-symbols-upload.ts +++ b/src/commands/crashlytics-symbols-upload.ts @@ -1,33 +1,26 @@ -import * as fs from "fs-extra"; import * as os from "os"; import * as path from "path"; -import * as spawn from "cross-spawn"; import * as uuid from "uuid"; import { Command } from "../command"; -import * as downloadUtils from "../downloadUtils"; import { FirebaseError } from "../error"; -import { logger } from "../logger"; -import * as rimraf from "rimraf"; import * as utils from "../utils"; +import { fetchBuildtoolsJar, runBuildtoolsCommand } from "../crashlytics/buildToolsJarHelper"; +import { Options } from "../options"; + enum SymbolGenerator { breakpad = "breakpad", csym = "csym", } -interface Options { - app: string | null; - generator: SymbolGenerator | null; - dryRun: boolean | null; - debug: boolean | null; - // Temporary override to use a local JAR until we get the fat jar in our - // bucket - localJar: string | null; +interface CommandOptions extends Options { + app?: string; + generator?: SymbolGenerator; + dryRun?: boolean; } interface JarOptions { - jarFile: string; app: string; generator: SymbolGenerator; cachePath: string; @@ -36,33 +29,21 @@ interface JarOptions { } const SYMBOL_CACHE_ROOT_DIR = process.env.FIREBASE_CRASHLYTICS_CACHE_PATH || os.tmpdir(); -const JAR_CACHE_DIR = - process.env.FIREBASE_CRASHLYTICS_BUILDTOOLS_PATH || - path.join(os.homedir(), ".cache", "firebase", "crashlytics", "buildtools"); -const JAR_VERSION = "2.8.0"; -const JAR_URL = `https://dl.google.com/android/maven2/com/google/firebase/firebase-crashlytics-buildtools/${JAR_VERSION}/firebase-crashlytics-buildtools-${JAR_VERSION}.jar`; - -export default new Command("crashlytics:symbols:upload ") - .description("Upload symbols for native code, to symbolicate stack traces.") + +export const command = new Command("crashlytics:symbols:upload ") + .description("upload symbols for native code, to symbolicate stack traces") .option("--app ", "the app id of your Firebase app") - .option("--generator [breakpad|csym]", "the symbol generator being used, defaults to breakpad.") + .option("--generator [breakpad|csym]", "the symbol generator being used, default is breakpad") .option("--dry-run", "generate symbols without uploading them") - .option("--debug", "print debug output and logging from the underlying uploader tool") - .action(async (symbolFiles: string[], options: Options) => { - const app = getGoogleAppID(options) || ""; + .action(async (symbolFiles: string[], options: CommandOptions) => { + const app = getGoogleAppID(options); const generator = getSymbolGenerator(options); const dryRun = !!options.dryRun; const debug = !!options.debug; - // If you set LOCAL_JAR to a path it will override the downloaded - // buildtools.jar - let jarFile = await downloadBuiltoolsJar(); - if (process.env.LOCAL_JAR) { - jarFile = process.env.LOCAL_JAR; - } + const jarFile = await fetchBuildtoolsJar(); const jarOptions: JarOptions = { - jarFile, app, generator, cachePath: path.join( @@ -71,7 +52,7 @@ export default new Command("crashlytics:symbols:upload ") "nativeSymbols", // Windows does not allow ":" in their directory names app.replace(/:/g, "-"), - generator + generator, ), symbolFile: "", generate: true, @@ -80,13 +61,9 @@ export default new Command("crashlytics:symbols:upload ") for (const symbolFile of symbolFiles) { utils.logBullet(`Generating symbols for ${symbolFile}`); const generateArgs = buildArgs({ ...jarOptions, symbolFile }); - const output = runJar(generateArgs, debug); - if (output.length > 0) { - utils.logBullet(output); - } else { - utils.logBullet(`Generated symbols for ${symbolFile}`); - utils.logBullet(`Output Path: ${jarOptions.cachePath}`); - } + runBuildtoolsCommand(jarFile, generateArgs, debug); + utils.logBullet(`Generated symbols for ${symbolFile}`); + utils.logBullet(`Output Path: ${jarOptions.cachePath}`); } if (dryRun) { @@ -94,24 +71,22 @@ export default new Command("crashlytics:symbols:upload ") return; } - utils.logBullet(`Uploading all generated symbols`); + utils.logBullet(`Uploading all generated symbols...`); const uploadArgs = buildArgs({ ...jarOptions, generate: false }); - const output = runJar(uploadArgs, debug); - if (output.length > 0) { - utils.logBullet(output); - } else { - utils.logBullet("Successfully uploaded all symbols"); - } + runBuildtoolsCommand(jarFile, uploadArgs, debug); + utils.logBullet("Successfully uploaded all symbols"); }); -function getGoogleAppID(options: Options): string | null { +function getGoogleAppID(options: CommandOptions): string { if (!options.app) { - throw new FirebaseError("set the --app option to a valid Firebase app id and try again"); + throw new FirebaseError( + "set --app to a valid Firebase application id, e.g. 1:00000000:android:0000000", + ); } return options.app; } -function getSymbolGenerator(options: Options): SymbolGenerator { +function getSymbolGenerator(options: CommandOptions): SymbolGenerator { // Default to using BreakPad symbols if (!options.generator) { return SymbolGenerator.breakpad; @@ -122,81 +97,18 @@ function getSymbolGenerator(options: Options): SymbolGenerator { return options.generator; } -async function downloadBuiltoolsJar(): Promise { - const jarPath = path.join(JAR_CACHE_DIR, `crashlytics-buildtools-${JAR_VERSION}.jar`); - if (fs.existsSync(jarPath)) { - logger.debug(`Buildtools Jar already downloaded at ${jarPath}`); - return jarPath; - } - // If the Jar cache directory exists, but the jar for the current version - // doesn't, then we're running the CLI with a new Jar version and we can - // delete the old version. - if (fs.existsSync(JAR_CACHE_DIR)) { - logger.debug( - `Deleting Jar cache at ${JAR_CACHE_DIR} because the CLI was run with a newer Jar version` - ); - rimraf.sync(JAR_CACHE_DIR); - } - utils.logBullet("Downloading buildtools.jar to " + jarPath); - utils.logBullet( - "For open source licenses used by this command, look in the META-INF directory in the buildtools.jar file" - ); - const tmpfile = await downloadUtils.downloadToTmp(JAR_URL); - fs.mkdirSync(JAR_CACHE_DIR, { recursive: true }); - fs.copySync(tmpfile, jarPath); - return jarPath; -} - function buildArgs(options: JarOptions): string[] { const baseArgs = [ - "-jar", - options.jarFile, - `-symbolGenerator=${options.generator}`, - `-symbolFileCacheDir=${options.cachePath}`, + "-symbolGenerator", + options.generator, + "-symbolFileCacheDir", + options.cachePath, "-verbose", ]; if (options.generate) { - return baseArgs.concat(["-generateNativeSymbols", `-unstrippedLibrary=${options.symbolFile}`]); + return baseArgs.concat(["-generateNativeSymbols", "-unstrippedLibrary", options.symbolFile]); } - return baseArgs.concat([ - "-uploadNativeSymbols", - `-googleAppId=${options.app}`, - // `-androidApplicationId=`, - ]); -} - -function runJar(args: string[], debug: boolean): string { - // Inherit is better for debug output because it'll print as it goes. If we - // pipe here and print after it'll wait until the command has finished to - // print all the output. - const outputs = spawn.sync("java", args, { - stdio: debug ? "inherit" : "pipe", - }); - - if (outputs.status || 0 > 0) { - if (!debug) { - utils.logWarning(outputs.stdout?.toString() || "An unknown error occurred"); - } - throw new FirebaseError("Failed to upload symbols"); - } - - // This is a bit gross, but since we don't have a great way to communicate - // between the buildtools.jar and the CLI, we just pull the logs out of the - // jar output. - if (!debug) { - let logRegex = /(Generated symbol file.*$)/m; - let matched = (outputs.stdout?.toString() || "").match(logRegex); - if (matched) { - return matched[1]; - } - logRegex = /(Crashlytics symbol file uploaded successfully.*$)/m; - matched = (outputs.stdout?.toString() || "").match(logRegex); - if (matched) { - return matched[1]; - } - return ""; - } - return ""; + return baseArgs.concat(["-uploadNativeSymbols", "-googleAppId", options.app]); } diff --git a/src/commands/database-get.ts b/src/commands/database-get.ts index 71e60e66397..1dfa0e93afb 100644 --- a/src/commands/database-get.ts +++ b/src/commands/database-get.ts @@ -11,7 +11,7 @@ import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api"; import { requirePermissions } from "../requirePermissions"; import { logger } from "../logger"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; -import * as responseToError from "../responseToError"; +import { responseToError } from "../responseToError"; import * as utils from "../utils"; /** @@ -26,7 +26,7 @@ function applyStringOpts( dest: { [key: string]: string }, src: { [key: string]: string }, keys: string[], - jsonKeys: string[] + jsonKeys: string[], ): void { for (const key of keys) { if (src[key]) { @@ -47,7 +47,7 @@ function applyStringOpts( } } -export default new Command("database:get ") +export const command = new Command("database:get ") .description("fetch and print JSON data at the specified path") .option("-o, --output ", "save output to the specified file") .option("--pretty", "pretty print response") @@ -63,7 +63,7 @@ export default new Command("database:get ") .option("--equal-to ", "restrict results to (based on specified ordering)") .option( "--instance ", - "use the database .firebaseio.com (if omitted, use default database instance)" + "use the database .firebaseio.com (if omitted, use default database instance)", ) .before(requirePermissions, ["firebasedatabase.instances.get"]) .before(requireDatabaseInstance) @@ -97,7 +97,7 @@ export default new Command("database:get ") query, options, ["limitToFirst", "limitToLast"], - ["orderBy", "startAt", "endAt", "equalTo"] + ["orderBy", "startAt", "endAt", "equalTo"], ); const urlObj = new url.URL(dbUrl); @@ -122,7 +122,7 @@ export default new Command("database:get ") let d; try { d = JSON.parse(r); - } catch (e) { + } catch (e: any) { throw new FirebaseError("Malformed JSON response", { original: e, exit: 2 }); } throw responseToError({ statusCode: res.status }, d); @@ -130,7 +130,7 @@ export default new Command("database:get ") res.body.pipe(outStream, { end: false }); - return new Promise((resolve) => { + return new Promise((resolve) => { // Tack on a single newline at the end of the stream. res.body.once("end", () => { if (outStream === process.stdout) { diff --git a/src/commands/database-import.ts b/src/commands/database-import.ts new file mode 100644 index 00000000000..db5dfd5726b --- /dev/null +++ b/src/commands/database-import.ts @@ -0,0 +1,119 @@ +import * as clc from "colorette"; +import * as fs from "fs"; +import * as utils from "../utils"; + +import { Command } from "../command"; +import DatabaseImporter from "../database/import"; +import { Emulators } from "../emulator/types"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { needProjectId } from "../projectUtils"; +import { Options } from "../options"; +import { printNoticeIfEmulated } from "../emulator/commandUtils"; +import { promptOnce } from "../prompt"; +import { DatabaseInstance, populateInstanceDetails } from "../management/database"; +import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api"; +import { requireDatabaseInstance } from "../requireDatabaseInstance"; +import { requirePermissions } from "../requirePermissions"; + +interface DatabaseImportOptions extends Options { + instance: string; + instanceDetails: DatabaseInstance; + disableTriggers?: boolean; + filter?: string; + chunkSize?: string; + concurrency?: string; +} + +const MAX_CHUNK_SIZE_MB = 1; +const MAX_PAYLOAD_SIZE_MB = 256; +const CONCURRENCY_LIMIT = 5; + +export const command = new Command("database:import [infile]") + .description( + "non-atomically import the contents of a JSON file to the specified path in Realtime Database", + ) + .withForce() + .option( + "--instance ", + "use the database .firebaseio.com (if omitted, use default database instance)", + ) + .option( + "--disable-triggers", + "suppress any Cloud functions triggered by this operation, default to true", + true, + ) + .option( + "--filter ", + "import only data at this path in the JSON file (if omitted, import entire file)", + ) + .option("--chunk-size ", "max chunk size in megabytes, default to 1 MB") + .option("--concurrency ", "concurrency limit, default to 5") + .before(requirePermissions, ["firebasedatabase.instances.update"]) + .before(requireDatabaseInstance) + .before(populateInstanceDetails) + .before(printNoticeIfEmulated, Emulators.DATABASE) + .action(async (path: string, infile: string | undefined, options: DatabaseImportOptions) => { + if (!path.startsWith("/")) { + throw new FirebaseError("Path must begin with /"); + } + + if (!infile) { + throw new FirebaseError("No file supplied"); + } + + const chunkMegabytes = options.chunkSize ? parseInt(options.chunkSize, 10) : MAX_CHUNK_SIZE_MB; + if (chunkMegabytes > MAX_PAYLOAD_SIZE_MB) { + throw new FirebaseError("Max chunk size cannot exceed 256 MB"); + } + + const projectId = needProjectId(options); + const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl); + const dbPath = utils.getDatabaseUrl(origin, options.instance, path); + const dbUrl = new URL(dbPath); + if (options.disableTriggers) { + dbUrl.searchParams.set("disableTriggers", "true"); + } + + const confirm = await promptOnce( + { + type: "confirm", + name: "force", + default: false, + message: "You are about to import data to " + clc.cyan(dbPath) + ". Are you sure?", + }, + options, + ); + if (!confirm) { + throw new FirebaseError("Command aborted."); + } + + const inStream = fs.createReadStream(infile); + const dataPath = options.filter || ""; + const chunkBytes = chunkMegabytes * 1024 * 1024; + const concurrency = options.concurrency ? parseInt(options.concurrency, 10) : CONCURRENCY_LIMIT; + const importer = new DatabaseImporter(dbUrl, inStream, dataPath, chunkBytes, concurrency); + + let responses; + try { + responses = await importer.execute(); + } catch (err: any) { + if (err instanceof FirebaseError) { + throw err; + } + logger.debug(err); + throw new FirebaseError(`Unexpected error while importing data: ${err}`, { exit: 2 }); + } + + if (responses.length) { + utils.logSuccess("Data persisted successfully"); + } else { + utils.logWarning("No data was persisted. Check the data path supplied."); + } + + logger.info(); + logger.info( + clc.bold("View data at:"), + utils.getDatabaseViewDataUrl(origin, projectId, options.instance, path), + ); + }); diff --git a/src/commands/database-instances-create.ts b/src/commands/database-instances-create.ts index c67a0eaaced..f860a232d0b 100644 --- a/src/commands/database-instances-create.ts +++ b/src/commands/database-instances-create.ts @@ -14,11 +14,11 @@ import { getDefaultDatabaseInstance } from "../getDefaultDatabaseInstance"; import { FirebaseError } from "../error"; import { MISSING_DEFAULT_INSTANCE_ERROR_MESSAGE } from "../requireDatabaseInstance"; -export default new Command("database:instances:create ") +export const command = new Command("database:instances:create ") .description("create a realtime database instance") .option( "-l, --location ", - "(optional) location for the database instance, defaults to us-central1" + "(optional) location for the database instance, defaults to us-central1", ) .before(requirePermissions, ["firebasedatabase.instances.create"]) .before(warnEmulatorNotSupported, Emulators.DATABASE) @@ -34,7 +34,7 @@ export default new Command("database:instances:create ") projectId, instanceName, location, - DatabaseInstanceType.USER_DATABASE + DatabaseInstanceType.USER_DATABASE, ); logger.info(`created database instance ${instance.name}`); return instance; diff --git a/src/commands/database-instances-list.ts b/src/commands/database-instances-list.ts index 465a2115351..faafdbfa2ea 100644 --- a/src/commands/database-instances-list.ts +++ b/src/commands/database-instances-list.ts @@ -1,15 +1,14 @@ +const Table = require("cli-table"); + import { Command } from "../command"; -import Table = require("cli-table"); -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as ora from "ora"; import { logger } from "../logger"; import { requirePermissions } from "../requirePermissions"; -import { needProjectNumber } from "../projectUtils"; -import firedata = require("../gcp/firedata"); import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -import { previews } from "../previews"; +import * as experiments from "../experiments"; import { needProjectId } from "../projectUtils"; import { listDatabaseInstances, @@ -18,72 +17,48 @@ import { parseDatabaseLocation, } from "../management/database"; -function logInstances(instances: DatabaseInstance[]): void { - if (instances.length === 0) { - logger.info(clc.bold("No database instances found.")); - return; - } - const tableHead = ["Database Instance Name", "Location", "Type", "State"]; - const table = new Table({ head: tableHead, style: { head: ["green"] } }); - instances.forEach((db) => { - table.push([db.name, db.location, db.type, db.state]); - }); - - logger.info(table.toString()); -} - -function logInstancesCount(count = 0): void { - if (count === 0) { - return; - } - logger.info(""); - logger.info(`${count} database instance(s) total.`); -} - -let cmd = new Command("database:instances:list") +export const command = new Command("database:instances:list") .description("list realtime database instances, optionally filtered by a specified location") .before(requirePermissions, ["firebasedatabase.instances.list"]) + .option( + "-l, --location ", + "(optional) location for the database instance, defaults to all regions", + ) .before(warnEmulatorNotSupported, Emulators.DATABASE) .action(async (options: any) => { const location = parseDatabaseLocation(options.location, DatabaseLocation.ANY); const spinner = ora( "Preparing the list of your Firebase Realtime Database instances" + - `${location === DatabaseLocation.ANY ? "" : ` for location: ${location}`}` + `${location === DatabaseLocation.ANY ? "" : ` for location: ${location}`}`, ).start(); - let instances; - if (previews.rtdbmanagement) { - const projectId = needProjectId(options); - try { - instances = await listDatabaseInstances(projectId, location); - } catch (err) { - spinner.fail(); - throw err; - } - spinner.succeed(); - logInstances(instances); - logInstancesCount(instances.length); - return instances; - } - const projectNumber = await needProjectNumber(options); + const projectId = needProjectId(options); + let instances: DatabaseInstance[] = []; try { - instances = await firedata.listDatabaseInstances(projectNumber); - } catch (err) { + instances = await listDatabaseInstances(projectId, location); + } catch (err: any) { spinner.fail(); throw err; } spinner.succeed(); - for (const instance of instances) { - logger.info(instance.instance); + if (instances.length === 0) { + logger.info(clc.bold("No database instances found.")); + return; + } + // TODO: remove rtdbmanagement experiment in the next major release. + if (!experiments.isEnabled("rtdbmanagement")) { + for (const instance of instances) { + logger.info(instance.name); + } + logger.info(`Project ${options.project} has ${instances.length} database instances`); + return instances; } - logger.info(`Project ${options.project} has ${instances.length} database instances`); + const tableHead = ["Database Instance Name", "Location", "Type", "State"]; + const table = new Table({ head: tableHead, style: { head: ["green"] } }); + for (const db of instances) { + table.push([db.name, db.location, db.type, db.state]); + } + logger.info(table.toString()); + logger.info(`${instances.length} database instance(s) total.`); return instances; }); - -if (previews.rtdbmanagement) { - cmd = cmd.option( - "-l, --location ", - "(optional) location for the database instance, defaults to us-central1" - ); -} -export default cmd; diff --git a/src/commands/database-profile.ts b/src/commands/database-profile.ts index 240c96efc89..68fa9fb662f 100644 --- a/src/commands/database-profile.ts +++ b/src/commands/database-profile.ts @@ -1,5 +1,3 @@ -import * as _ from "lodash"; - import { Command } from "../command"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; import { populateInstanceDetails } from "../management/database"; @@ -11,23 +9,23 @@ import { warnEmulatorNotSupported } from "../emulator/commandUtils"; const description = "profile the Realtime Database and generate a usage report"; -module.exports = new Command("database:profile") +export const command = new Command("database:profile") .description(description) .option("-o, --output ", "save the output to the specified file") .option( "-d, --duration ", - "collect database usage information for the specified number of seconds" + "collect database usage information for the specified number of seconds", ) .option("--raw", "output the raw stats collected as newline delimited json") .option("--no-collapse", "prevent collapsing similar paths into $wildcard locations") .option( "-i, --input ", "generate the report based on the specified file instead " + - "of streaming logs from the database" + "of streaming logs from the database", ) .option( "--instance ", - "use the database .firebaseio.com (if omitted, use default database instance)" + "use the database .firebaseio.com (if omitted, use default database instance)", ) .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) @@ -41,11 +39,11 @@ module.exports = new Command("database:profile") }); } else if (options.parent.json && options.raw) { return utils.reject("Cannot output raw data in json format", { exit: 1 }); - } else if (options.input && _.has(options, "duration")) { + } else if (options.input && options.duration !== undefined) { return utils.reject("Cannot specify a duration for input files", { exit: 1, }); - } else if (_.has(options, "duration") && options.duration <= 0) { + } else if (options.duration !== undefined && options.duration <= 0) { return utils.reject("Must specify a positive number of seconds", { exit: 1, }); diff --git a/src/commands/database-push.ts b/src/commands/database-push.ts index dc2e43dbc36..0551970a684 100644 --- a/src/commands/database-push.ts +++ b/src/commands/database-push.ts @@ -1,5 +1,4 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import { Client } from "../apiv2"; @@ -15,25 +14,29 @@ import { logger } from "../logger"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; import * as utils from "../utils"; -export default new Command("database:push [infile]") +export const command = new Command("database:push [infile]") .description("add a new JSON object to a list of data in your Firebase") .option("-d, --data ", "specify escaped JSON directly") .option( "--instance ", - "use the database .firebaseio.com (if omitted, use default database instance)" + "use the database .firebaseio.com (if omitted, use default database instance)", ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) .before(printNoticeIfEmulated, Emulators.DATABASE) - .action(async (path, infile, options) => { - if (!_.startsWith(path, "/")) { + .action(async (path: string, infile, options) => { + if (!path.startsWith("/")) { throw new FirebaseError("Path must begin with /"); } const inStream = utils.stringToStream(options.data) || (infile ? fs.createReadStream(infile) : process.stdin); const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl); const u = new URL(utils.getDatabaseUrl(origin, options.instance, path + ".json")); + if (options.disableTriggers) { + u.searchParams.set("disableTriggers", "true"); + } if (!infile && !options.data) { utils.explainStdin(); @@ -47,13 +50,14 @@ export default new Command("database:push [infile]") method: "POST", path: u.pathname, body: inStream, + queryParams: u.searchParams, }); - } catch (err) { + } catch (err: any) { logger.debug(err); throw new FirebaseError(`Unexpected error while pushing data: ${err}`, { exit: 2 }); } - if (!_.endsWith(path, "/")) { + if (!path.endsWith("/")) { path += "/"; } @@ -61,7 +65,7 @@ export default new Command("database:push [infile]") origin, options.project, options.instance, - path + res.body.name + path + res.body.name, ); utils.logSuccess("Data pushed successfully"); diff --git a/src/commands/database-remove.ts b/src/commands/database-remove.ts index b8f7389292e..f7fd639f25d 100644 --- a/src/commands/database-remove.ts +++ b/src/commands/database-remove.ts @@ -8,22 +8,22 @@ import { populateInstanceDetails } from "../management/database"; import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api"; import * as utils from "../utils"; import { promptOnce } from "../prompt"; -import * as clc from "cli-color"; -import * as _ from "lodash"; +import * as clc from "colorette"; -module.exports = new Command("database:remove ") +export const command = new Command("database:remove ") .description("remove data from your Firebase at the specified path") .option("-f, --force", "pass this option to bypass confirmation prompt") .option( "--instance ", - "use the database .firebaseio.com (if omitted, use default database instance)" + "use the database .firebaseio.com (if omitted, use default database instance)", ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) .before(warnEmulatorNotSupported, Emulators.DATABASE) - .action(async (path, options) => { - if (!_.startsWith(path, "/")) { + .action(async (path: string, options) => { + if (!path.startsWith("/")) { return utils.reject("Path must begin with /", { exit: 1 }); } const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl); @@ -35,13 +35,13 @@ module.exports = new Command("database:remove ") default: false, message: "You are about to remove all data at " + clc.cyan(databaseUrl) + ". Are you sure?", }, - options + options, ); if (!confirm) { return utils.reject("Command aborted.", { exit: 1 }); } - const removeOps = new DatabaseRemove(options.instance, path, origin); + const removeOps = new DatabaseRemove(options.instance, path, origin, !!options.disableTriggers); await removeOps.execute(); utils.logSuccess("Data removed successfully"); }); diff --git a/src/commands/database-rules-canary.ts b/src/commands/database-rules-canary.ts index 7b790b82a19..cf9d626b202 100644 --- a/src/commands/database-rules-canary.ts +++ b/src/commands/database-rules-canary.ts @@ -5,11 +5,11 @@ import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; -export default new Command("database:rules:canary ") +export const command = new Command("database:rules:canary ") .description("mark a staged ruleset as the canary ruleset") .option( "--instance ", - "use the database .firebaseio.com (if omitted, uses default database instance)" + "use the database .firebaseio.com (if omitted, uses default database instance)", ) .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) diff --git a/src/commands/database-rules-get.ts b/src/commands/database-rules-get.ts index a76401f7d10..cf11f72814f 100644 --- a/src/commands/database-rules-get.ts +++ b/src/commands/database-rules-get.ts @@ -6,11 +6,11 @@ import * as metadata from "../database/metadata"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -export default new Command("database:rules:get ") +export const command = new Command("database:rules:get ") .description("get a realtime database ruleset by id") .option( "--instance ", - "use the database .firebaseio.com (if omitted, uses default database instance)" + "use the database .firebaseio.com (if omitted, uses default database instance)", ) .before(requirePermissions, ["firebasedatabase.instances.get"]) .before(requireDatabaseInstance) diff --git a/src/commands/database-rules-list.ts b/src/commands/database-rules-list.ts index 2c1fdacfd00..8573118757c 100644 --- a/src/commands/database-rules-list.ts +++ b/src/commands/database-rules-list.ts @@ -6,11 +6,11 @@ import * as metadata from "../database/metadata"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -export default new Command("database:rules:list") +export const command = new Command("database:rules:list") .description("list realtime database rulesets") .option( "--instance ", - "use the database .firebaseio.com (if omitted, uses default database instance)" + "use the database .firebaseio.com (if omitted, uses default database instance)", ) .before(requirePermissions, ["firebasedatabase.instances.get"]) .before(requireDatabaseInstance) @@ -20,10 +20,10 @@ export default new Command("database:rules:list") const rulesets = await metadata.listAllRulesets(options.instance); for (const ruleset of rulesets) { const labels = []; - if (ruleset.id == labeled.stable) { + if (ruleset.id === labeled.stable) { labels.push("stable"); } - if (ruleset.id == labeled.canary) { + if (ruleset.id === labeled.canary) { labels.push("canary"); } logger.info(`${ruleset.id} ${ruleset.createdAt} ${labels.join(",")}`); diff --git a/src/commands/database-rules-release.ts b/src/commands/database-rules-release.ts index 07e503eced5..ff68cdfaa56 100644 --- a/src/commands/database-rules-release.ts +++ b/src/commands/database-rules-release.ts @@ -5,11 +5,11 @@ import * as metadata from "../database/metadata"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -export default new Command("database:rules:release ") +export const command = new Command("database:rules:release ") .description("mark a staged ruleset as the stable ruleset") .option( "--instance ", - "use the database .firebaseio.com (if omitted, uses default database instance)" + "use the database .firebaseio.com (if omitted, uses default database instance)", ) .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) diff --git a/src/commands/database-rules-stage.ts b/src/commands/database-rules-stage.ts index c12428a964e..030bb8fe0da 100644 --- a/src/commands/database-rules-stage.ts +++ b/src/commands/database-rules-stage.ts @@ -8,11 +8,11 @@ import * as path from "path"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -export default new Command("database:rules:stage") +export const command = new Command("database:rules:stage") .description("create a new realtime database ruleset") .option( "--instance ", - "use the database .firebaseio.com (if omitted, uses default database instance)" + "use the database .firebaseio.com (if omitted, uses default database instance)", ) .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) diff --git a/src/commands/database-set.ts b/src/commands/database-set.ts index 33ea976b6ee..b53762ce43c 100644 --- a/src/commands/database-set.ts +++ b/src/commands/database-set.ts @@ -1,5 +1,4 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import { Client } from "../apiv2"; @@ -16,25 +15,29 @@ import { logger } from "../logger"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; import * as utils from "../utils"; -export default new Command("database:set [infile]") +export const command = new Command("database:set [infile]") .description("store JSON data at the specified path via STDIN, arg, or file") .option("-d, --data ", "specify escaped JSON directly") .option("-f, --force", "pass this option to bypass confirmation prompt") .option( "--instance ", - "use the database .firebaseio.com (if omitted, use default database instance)" + "use the database .firebaseio.com (if omitted, use default database instance)", ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) .before(printNoticeIfEmulated, Emulators.DATABASE) - .action(async (path, infile, options) => { - if (!_.startsWith(path, "/")) { + .action(async (path: string, infile, options) => { + if (!path.startsWith("/")) { throw new FirebaseError("Path must begin with /"); } const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl); const dbPath = utils.getDatabaseUrl(origin, options.instance, path); const dbJsonURL = new URL(utils.getDatabaseUrl(origin, options.instance, path + ".json")); + if (options.disableTriggers) { + dbJsonURL.searchParams.set("disableTriggers", "true"); + } const confirm = await promptOnce( { @@ -43,7 +46,7 @@ export default new Command("database:set [infile]") default: false, message: "You are about to overwrite all data at " + clc.cyan(dbPath) + ". Are you sure?", }, - options + options, ); if (!confirm) { throw new FirebaseError("Command aborted."); @@ -62,8 +65,9 @@ export default new Command("database:set [infile]") method: "PUT", path: dbJsonURL.pathname, body: inStream, + queryParams: dbJsonURL.searchParams, }); - } catch (err) { + } catch (err: any) { logger.debug(err); throw new FirebaseError(`Unexpected error while setting data: ${err}`, { exit: 2 }); } @@ -72,6 +76,6 @@ export default new Command("database:set [infile]") logger.info(); logger.info( clc.bold("View data at:"), - utils.getDatabaseViewDataUrl(origin, options.project, options.instance, path) + utils.getDatabaseViewDataUrl(origin, options.project, options.instance, path), ); }); diff --git a/src/commands/database-settings-get.ts b/src/commands/database-settings-get.ts index da21a6d3208..46281c2e0a5 100644 --- a/src/commands/database-settings-get.ts +++ b/src/commands/database-settings-get.ts @@ -12,11 +12,11 @@ import { warnEmulatorNotSupported } from "../emulator/commandUtils"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; import * as utils from "../utils"; -export default new Command("database:settings:get ") +export const command = new Command("database:settings:get ") .description("read the realtime database setting at path") .option( "--instance ", - "use the database .firebaseio.com (if omitted, uses default database instance)" + "use the database .firebaseio.com (if omitted, uses default database instance)", ) .help(HELP_TEXT) .before(requirePermissions, ["firebasedatabase.instances.get"]) @@ -33,14 +33,14 @@ export default new Command("database:settings:get ") utils.getDatabaseUrl( realtimeOriginOrCustomUrl(options.instanceDetails.databaseUrl), options.instance, - `/.settings/${path}.json` - ) + `/.settings/${path}.json`, + ), ); const c = new Client({ urlPrefix: u.origin, auth: true }); let res; try { res = await c.get(u.pathname); - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Unexpected error fetching configs at ${path}`, { exit: 2, original: err, @@ -53,5 +53,5 @@ export default new Command("database:settings:get ") res.body = (res.body as any).value; } utils.logSuccess(`For database instance ${options.instance}\n\t ${path} = ${res.body}`); - } + }, ); diff --git a/src/commands/database-settings-set.ts b/src/commands/database-settings-set.ts index e0b3575cd8c..eacd478bd1a 100644 --- a/src/commands/database-settings-set.ts +++ b/src/commands/database-settings-set.ts @@ -12,11 +12,11 @@ import { warnEmulatorNotSupported } from "../emulator/commandUtils"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; import * as utils from "../utils"; -export default new Command("database:settings:set ") +export const command = new Command("database:settings:set ") .description("set the realtime database setting at path.") .option( "--instance ", - "use the database .firebaseio.com (if omitted, use default database instance)" + "use the database .firebaseio.com (if omitted, use default database instance)", ) .help(HELP_TEXT) .before(requirePermissions, ["firebasedatabase.instances.update"]) @@ -38,13 +38,13 @@ export default new Command("database:settings:set ") utils.getDatabaseUrl( realtimeOriginOrCustomUrl(options.instanceDetails.databaseUrl), options.instance, - `/.settings/${path}.json` - ) + `/.settings/${path}.json`, + ), ); const c = new Client({ urlPrefix: u.origin, auth: true }); try { await c.put(u.pathname, JSON.stringify(parsedValue)); - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Unexpected error fetching configs at ${path}`, { exit: 2, original: err, @@ -52,6 +52,6 @@ export default new Command("database:settings:set ") } utils.logSuccess("Successfully set setting."); utils.logSuccess( - `For database instance ${options.instance}\n\t ${path} = ${JSON.stringify(parsedValue)}` + `For database instance ${options.instance}\n\t ${path} = ${JSON.stringify(parsedValue)}`, ); }); diff --git a/src/commands/database-update.ts b/src/commands/database-update.ts index ef4259e17e2..8ef2848e563 100644 --- a/src/commands/database-update.ts +++ b/src/commands/database-update.ts @@ -1,5 +1,5 @@ import { URL } from "url"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import { Client } from "../apiv2"; @@ -15,14 +15,15 @@ import { logger } from "../logger"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; import * as utils from "../utils"; -export default new Command("database:update [infile]") +export const command = new Command("database:update [infile]") .description("update some of the keys for the defined path in your Firebase") .option("-d, --data ", "specify escaped JSON directly") .option("-f, --force", "pass this option to bypass confirmation prompt") .option( "--instance ", - "use the database .firebaseio.com (if omitted, use default database instance)" + "use the database .firebaseio.com (if omitted, use default database instance)", ) + .option("--disable-triggers", "suppress any Cloud functions triggered by this operation") .before(requirePermissions, ["firebasedatabase.instances.update"]) .before(requireDatabaseInstance) .before(populateInstanceDetails) @@ -40,7 +41,7 @@ export default new Command("database:update [infile]") default: false, message: `You are about to modify data at ${clc.cyan(url)}. Are you sure?`, }, - options + options, ); if (!confirmed) { throw new FirebaseError("Command aborted."); @@ -51,6 +52,9 @@ export default new Command("database:update [infile]") (infile && fs.createReadStream(infile)) || process.stdin; const jsonUrl = new URL(utils.getDatabaseUrl(origin, options.instance, path + ".json")); + if (options.disableTriggers) { + jsonUrl.searchParams.set("disableTriggers", "true"); + } if (!infile && !options.data) { utils.explainStdin(); @@ -62,8 +66,9 @@ export default new Command("database:update [infile]") method: "PATCH", path: jsonUrl.pathname, body: inStream, + queryParams: jsonUrl.searchParams, }); - } catch (err) { + } catch (err: any) { throw new FirebaseError("Unexpected error while setting data"); } @@ -71,6 +76,6 @@ export default new Command("database:update [infile]") logger.info(); logger.info( clc.bold("View data at:"), - utils.getDatabaseViewDataUrl(origin, options.project, options.instance, path) + utils.getDatabaseViewDataUrl(origin, options.project, options.instance, path), ); }); diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts new file mode 100644 index 00000000000..36032274520 --- /dev/null +++ b/src/commands/dataconnect-sdk-generate.ts @@ -0,0 +1,51 @@ +import * as path from "path"; +import * as clc from "colorette"; + +import { Command } from "../command"; +import { Options } from "../options"; +import { DataConnectEmulator } from "../emulator/dataconnectEmulator"; +import { needProjectId } from "../projectUtils"; +import { load } from "../dataconnect/load"; +import { readFirebaseJson } from "../dataconnect/fileUtils"; +import { logger } from "../logger"; + +export const command = new Command("dataconnect:sdk:generate") + .description("generates typed SDKs for your Data Connect connectors") + .action(async (options: Options) => { + const projectId = needProjectId(options); + + const services = readFirebaseJson(options.config); + for (const service of services) { + let configDir = service.source; + if (!path.isAbsolute(configDir)) { + const cwd = options.cwd || process.cwd(); + configDir = path.resolve(path.join(cwd), configDir); + } + const serviceInfo = await load(projectId, configDir); + const hasGeneratables = serviceInfo.connectorInfo.some((c) => { + return ( + c.connectorYaml.generate?.javascriptSdk || + c.connectorYaml.generate?.kotlinSdk || + c.connectorYaml.generate?.swiftSdk + ); + }); + if (!hasGeneratables) { + logger.warn("No generated SDKs have been declared in connector.yaml files."); + logger.warn( + `Run ${clc.bold("firebase init dataconnect:sdk")} to configure a generated SDK.`, + ); + logger.warn( + `See https://firebase.google.com/docs/data-connect/gp/web-sdk for more details of how to configure generated SDKs.`, + ); + return; + } + for (const conn of serviceInfo.connectorInfo) { + const output = await DataConnectEmulator.generate({ + configDir, + connectorId: conn.connectorYaml.connectorId, + }); + logger.info(output); + logger.info(`Generated SDKs for ${conn.connectorYaml.connectorId}`); + } + } + }); diff --git a/src/commands/dataconnect-services-list.ts b/src/commands/dataconnect-services-list.ts new file mode 100644 index 00000000000..ccfd26130e7 --- /dev/null +++ b/src/commands/dataconnect-services-list.ts @@ -0,0 +1,73 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import * as names from "../dataconnect/names"; +import * as client from "../dataconnect/client"; +import { logger } from "../logger"; +import { requirePermissions } from "../requirePermissions"; +import { ensureApis } from "../dataconnect/ensureApis"; +const Table = require("cli-table"); + +export const command = new Command("dataconnect:services:list") + .description("list all deployed services in your Firebase project") + .before(requirePermissions, [ + "dataconnect.services.list", + "dataconnect.schemas.list", + "dataconnect.connectors.list", + ]) + .action(async (options: Options) => { + const projectId = needProjectId(options); + await ensureApis(projectId); + const services = await client.listAllServices(projectId); + const table: Record[] = new Table({ + head: [ + "Service ID", + "Location", + "Data Source", + "Schema Last Updated", + "Connector ID", + "Connector Last Updated", + ], + style: { head: ["yellow"] }, + }); + const jsonOutput: { services: Record[] } = { services: [] }; + for (const service of services) { + const schema = (await client.getSchema(service.name)) ?? { + name: "", + primaryDatasource: {}, + source: { files: [] }, + }; + const connectors = await client.listConnectors(service.name); + const serviceName = names.parseServiceName(service.name); + const instanceName = schema?.primaryDatasource.postgresql?.cloudSql.instance ?? ""; + const instanceId = instanceName.split("/").pop(); + const dbId = schema?.primaryDatasource.postgresql?.database ?? ""; + const dbName = `CloudSQL Instance: ${instanceId}\nDatabase:${dbId}`; + table.push([ + serviceName.serviceId, + serviceName.location, + dbName, + schema?.updateTime ?? "", + "", + "", + ]); + const serviceJson = { + serviceId: serviceName.serviceId, + location: serviceName.location, + datasource: dbName, + schemaUpdateTime: schema?.updateTime, + connectors: [] as { connectorId: string; connectorLastUpdated: string }[], + }; + for (const conn of connectors) { + const connectorName = names.parseConnectorName(conn.name); + table.push(["", "", "", "", connectorName.connectorId, conn.updateTime]); + serviceJson.connectors.push({ + connectorId: connectorName.connectorId, + connectorLastUpdated: conn.updateTime ?? "", + }); + } + jsonOutput.services.push(serviceJson); + } + logger.info(table.toString()); + return jsonOutput; + }); diff --git a/src/commands/dataconnect-sql-diff.ts b/src/commands/dataconnect-sql-diff.ts new file mode 100644 index 00000000000..204b7a170e5 --- /dev/null +++ b/src/commands/dataconnect-sql-diff.ts @@ -0,0 +1,27 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { ensureApis } from "../dataconnect/ensureApis"; +import { requirePermissions } from "../requirePermissions"; +import { pickService } from "../dataconnect/fileUtils"; +import { diffSchema } from "../dataconnect/schemaMigration"; +import { requireAuth } from "../requireAuth"; + +export const command = new Command("dataconnect:sql:diff [serviceId]") + .description( + "displays the differences between a local DataConnect schema and your CloudSQL database's current schema", + ) + .before(requirePermissions, [ + "firebasedataconnect.services.list", + "firebasedataconnect.schemas.list", + "firebasedataconnect.schemas.update", + ]) + .before(requireAuth) + .action(async (serviceId: string, options: Options) => { + const projectId = needProjectId(options); + await ensureApis(projectId); + const serviceInfo = await pickService(projectId, options.config, serviceId); + + const diffs = await diffSchema(serviceInfo.schema); + return { projectId, serviceId, diffs }; + }); diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts new file mode 100644 index 00000000000..77940d5a541 --- /dev/null +++ b/src/commands/dataconnect-sql-migrate.ts @@ -0,0 +1,48 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { pickService } from "../dataconnect/fileUtils"; +import { FirebaseError } from "../error"; +import { migrateSchema } from "../dataconnect/schemaMigration"; +import { requireAuth } from "../requireAuth"; +import { requirePermissions } from "../requirePermissions"; +import { ensureApis } from "../dataconnect/ensureApis"; +import { logLabeledSuccess } from "../utils"; + +export const command = new Command("dataconnect:sql:migrate [serviceId]") + .description("migrates your CloudSQL database's schema to match your local DataConnect schema") + .before(requirePermissions, [ + "firebasedataconnect.services.list", + "firebasedataconnect.schemas.list", + "firebasedataconnect.schemas.update", + "cloudsql.instances.connect", + "cloudsql.users.create", + ]) + .before(requireAuth) + .withForce("Execute any required database changes without prompting") + .action(async (serviceId: string, options: Options) => { + const projectId = needProjectId(options); + await ensureApis(projectId); + const serviceInfo = await pickService(projectId, options.config, serviceId); + const instanceId = + serviceInfo.dataConnectYaml.schema.datasource.postgresql?.cloudSql.instanceId; + if (!instanceId) { + throw new FirebaseError( + "dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId", + ); + } + const diffs = await migrateSchema({ + options, + schema: serviceInfo.schema, + validateOnly: true, + }); + if (diffs.length) { + logLabeledSuccess( + "dataconnect", + `Database schema sucessfully migrated! Run 'firebase deploy' to deploy your new schema to your Data Connect service.`, + ); + } else { + logLabeledSuccess("dataconnect", "Database schema is already up to date!"); + } + return { projectId, serviceId, diffs }; + }); diff --git a/src/commands/deploy.js b/src/commands/deploy.js deleted file mode 100644 index 864672ac911..00000000000 --- a/src/commands/deploy.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; - -const _ = require("lodash"); -const { requireDatabaseInstance } = require("../requireDatabaseInstance"); -const { requirePermissions } = require("../requirePermissions"); -const { checkServiceAccountIam } = require("../deploy/functions/checkIam"); -const checkValidTargetFilters = require("../checkValidTargetFilters"); -const { Command } = require("../command"); -const deploy = require("../deploy"); -const requireConfig = require("../requireConfig"); -const { filterTargets } = require("../filterTargets"); -const { requireHostingSite } = require("../requireHostingSite"); - -// in order of least time-consuming to most time-consuming -const VALID_TARGETS = [ - "database", - "storage", - "firestore", - "functions", - "hosting", - "remoteconfig", - "extensions", -]; -const TARGET_PERMISSIONS = { - database: ["firebasedatabase.instances.update"], - hosting: ["firebasehosting.sites.update"], - functions: [ - "cloudfunctions.functions.list", - "cloudfunctions.functions.create", - "cloudfunctions.functions.get", - "cloudfunctions.functions.update", - "cloudfunctions.functions.delete", - "cloudfunctions.operations.get", - ], - firestore: [ - "datastore.indexes.list", - "datastore.indexes.create", - "datastore.indexes.update", - "datastore.indexes.delete", - ], - storage: [ - "firebaserules.releases.create", - "firebaserules.rulesets.create", - "firebaserules.releases.update", - ], - remoteconfig: ["cloudconfig.configs.get", "cloudconfig.configs.update"], -}; - -module.exports = new Command("deploy") - .description("deploy code and assets to your Firebase project") - .withForce( - "delete Cloud Functions missing from the current working directory without confirmation" - ) - .option("-p, --public ", "override the Hosting public directory specified in firebase.json") - .option("-m, --message ", "an optional message describing this deploy") - .option( - "--only ", - 'only deploy to specified, comma-separated targets (e.g. "hosting,storage"). For functions, ' + - 'can specify filters with colons to scope function deploys to only those functions (e.g. "--only functions:func1,functions:func2"). ' + - "When filtering based on export groups (the exported module object keys), use dots to specify group names " + - '(e.g. "--only functions:group1.subgroup1,functions:group2)"' - ) - .option("--except ", 'deploy to all targets except specified (e.g. "database")') - .before(requireConfig) - .before(function (options) { - options.filteredTargets = filterTargets(options, VALID_TARGETS); - const permissions = options.filteredTargets.reduce((perms, target) => { - return perms.concat(TARGET_PERMISSIONS[target]); - }, []); - return requirePermissions(options, permissions); - }) - .before((options) => { - if (options.filteredTargets.includes("functions")) { - return checkServiceAccountIam(options.project); - } - }) - .before(async function (options) { - // only fetch the default instance for hosting or database deploys - if (_.includes(options.filteredTargets, "database")) { - await requireDatabaseInstance(options); - } - - if (_.includes(options.filteredTargets, "hosting")) { - await requireHostingSite(options); - } - }) - .before(checkValidTargetFilters) - .action(function (options) { - return deploy(options.filteredTargets, options); - }); diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts new file mode 100644 index 00000000000..1b42aa6f65a --- /dev/null +++ b/src/commands/deploy.ts @@ -0,0 +1,137 @@ +import { requireDatabaseInstance } from "../requireDatabaseInstance"; +import { requirePermissions } from "../requirePermissions"; +import { checkServiceAccountIam } from "../deploy/functions/checkIam"; +import { checkValidTargetFilters } from "../checkValidTargetFilters"; +import { Command } from "../command"; +import { deploy } from "../deploy"; +import { requireConfig } from "../requireConfig"; +import { filterTargets } from "../filterTargets"; +import { requireHostingSite } from "../requireHostingSite"; +import { errNoDefaultSite } from "../getDefaultHostingSite"; +import { FirebaseError } from "../error"; +import { bold } from "colorette"; +import { interactiveCreateHostingSite } from "../hosting/interactive"; +import { logBullet } from "../utils"; + +// in order of least time-consuming to most time-consuming +export const VALID_DEPLOY_TARGETS = [ + "database", + "storage", + "firestore", + "functions", + "hosting", + "remoteconfig", + "extensions", + "dataconnect", +]; +export const TARGET_PERMISSIONS: Record<(typeof VALID_DEPLOY_TARGETS)[number], string[]> = { + database: ["firebasedatabase.instances.update"], + hosting: ["firebasehosting.sites.update"], + functions: [ + "cloudfunctions.functions.list", + "cloudfunctions.functions.create", + "cloudfunctions.functions.get", + "cloudfunctions.functions.update", + "cloudfunctions.functions.delete", + "cloudfunctions.operations.get", + ], + firestore: [ + "datastore.indexes.list", + "datastore.indexes.create", + "datastore.indexes.update", + "datastore.indexes.delete", + ], + storage: [ + "firebaserules.releases.create", + "firebaserules.rulesets.create", + "firebaserules.releases.update", + ], + remoteconfig: ["cloudconfig.configs.get", "cloudconfig.configs.update"], + dataconnect: [ + "cloudsql.databases.create", + "cloudsql.databases.update", + "cloudsql.instances.connect", + "cloudsql.instances.create", // TODO: Support users who don't have cSQL writer permissions and want to use existing instances + "cloudsql.instances.get", + "cloudsql.instances.list", + "cloudsql.instances.update", + "cloudsql.users.create", + "firebasedataconnect.connectors.create", + "firebasedataconnect.connectors.delete", + "firebasedataconnect.connectors.list", + "firebasedataconnect.connectors.update", + "firebasedataconnect.operations.get", + "firebasedataconnect.services.create", + "firebasedataconnect.services.delete", + "firebasedataconnect.services.update", + "firebasedataconnect.services.list", + "firebasedataconnect.schemas.create", + "firebasedataconnect.schemas.delete", + "firebasedataconnect.schemas.list", + "firebasedataconnect.schemas.update", + ], +}; + +export const command = new Command("deploy") + .description("deploy code and assets to your Firebase project") + .withForce( + "delete Cloud Functions missing from the current working directory and bypass interactive prompts", + ) + .option("-p, --public ", "override the Hosting public directory specified in firebase.json") + .option("-m, --message ", "an optional message describing this deploy") + .option( + "--only ", + 'only deploy to specified, comma-separated targets (e.g. "hosting,storage"). For functions, ' + + 'can specify filters with colons to scope function deploys to only those functions (e.g. "--only functions:func1,functions:func2"). ' + + "When filtering based on export groups (the exported module object keys), use dots to specify group names " + + '(e.g. "--only functions:group1.subgroup1,functions:group2)"' + + "For data connect, can specify filters with colons to deploy only a service, connector, or schema" + + '(e.g. "--only dataconnect:serviceId,dataconnect:serviceId:connectorId,dataconnect:serviceId:schema"). ', + ) + .option("--except ", 'deploy to all targets except specified (e.g. "database")') + .before(requireConfig) + .before((options) => { + options.filteredTargets = filterTargets(options, VALID_DEPLOY_TARGETS); + const permissions = options.filteredTargets.reduce((perms: string[], target: string) => { + return perms.concat(TARGET_PERMISSIONS[target]); + }, []); + return requirePermissions(options, permissions); + }) + .before((options) => { + if (options.filteredTargets.includes("functions")) { + return checkServiceAccountIam(options.project); + } + }) + .before(async (options) => { + // only fetch the default instance for hosting or database deploys + if (options.filteredTargets.includes("database")) { + await requireDatabaseInstance(options); + } + + if (options.filteredTargets.includes("hosting")) { + let createSite = false; + try { + await requireHostingSite(options); + } catch (err: unknown) { + if (err === errNoDefaultSite) { + createSite = true; + } + } + if (!createSite) { + return; + } + if (options.nonInteractive) { + throw new FirebaseError( + `Unable to deploy to Hosting as there is no Hosting site. Use ${bold( + "firebase hosting:sites:create", + )} to create a site.`, + ); + } + logBullet("No Hosting site detected."); + await interactiveCreateHostingSite("", "", options); + } + }) + .before(checkValidTargetFilters) + .action((options) => { + return deploy(options.filteredTargets, options); + }); diff --git a/src/commands/emulators-exec.ts b/src/commands/emulators-exec.ts index f6166d732e1..6c023cbe098 100644 --- a/src/commands/emulators-exec.ts +++ b/src/commands/emulators-exec.ts @@ -1,15 +1,20 @@ import { Command } from "../command"; import * as commandUtils from "../emulator/commandUtils"; +import { emulatorExec, shutdownWhenKilled } from "../emulator/commandUtils"; -module.exports = new Command("emulators:exec -` +`, ); }); } diff --git a/src/test/emulators/auth/idp.spec.ts b/src/emulator/auth/idp.spec.ts similarity index 78% rename from src/test/emulators/auth/idp.spec.ts rename to src/emulator/auth/idp.spec.ts index e47010c6a8a..b02b270160a 100644 --- a/src/test/emulators/auth/idp.spec.ts +++ b/src/emulator/auth/idp.spec.ts @@ -1,8 +1,9 @@ import { expect } from "chai"; +import * as nock from "nock"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { PROVIDER_PASSWORD, SIGNIN_METHOD_EMAIL_LINK } from "../../../emulator/auth/state"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; +import { FirebaseJwtPayload } from "./operations"; +import { PROVIDER_PASSWORD, SIGNIN_METHOD_EMAIL_LINK } from "./state"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; import { expectStatusCode, getAccountInfoByIdToken, @@ -18,11 +19,18 @@ import { TEST_PHONE_NUMBER, FAKE_GOOGLE_ACCOUNT, REAL_GOOGLE_ACCOUNT, - TEST_MFA_INFO, enrollPhoneMfa, getAccountInfoByLocalId, registerTenant, -} from "./helpers"; + updateConfig, + BEFORE_CREATE_PATH, + BEFORE_CREATE_URL, + BEFORE_SIGN_IN_PATH, + BEFORE_SIGN_IN_URL, + BLOCKING_FUNCTION_HOST, + DISPLAY_NAME, + PHOTO_URL, +} from "./testing/helpers"; // Many JWT fields from IDPs use snake_case and we need to match that. @@ -43,7 +51,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { expect(res.body.email).to.equal(FAKE_GOOGLE_ACCOUNT.email); expect(res.body.emailVerified).to.equal(FAKE_GOOGLE_ACCOUNT.emailVerified); expect(res.body.federatedId).to.equal( - `https://accounts.google.com/${FAKE_GOOGLE_ACCOUNT.rawId}` + `https://accounts.google.com/${FAKE_GOOGLE_ACCOUNT.rawId}`, ); expect(res.body.oauthIdToken).to.equal(FAKE_GOOGLE_ACCOUNT.idToken); expect(res.body.providerId).to.equal("google.com"); @@ -68,7 +76,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { ]); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -101,7 +109,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { expect(res.body.email).to.equal(REAL_GOOGLE_ACCOUNT.email); expect(res.body.emailVerified).to.equal(REAL_GOOGLE_ACCOUNT.emailVerified); expect(res.body.federatedId).to.equal( - `https://accounts.google.com/${REAL_GOOGLE_ACCOUNT.rawId}` + `https://accounts.google.com/${REAL_GOOGLE_ACCOUNT.rawId}`, ); expect(res.body.oauthIdToken).to.equal(REAL_GOOGLE_ACCOUNT.idToken); expect(res.body.providerId).to.equal("google.com"); @@ -126,7 +134,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { ]); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -181,11 +189,11 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { expect(raw.family_name).to.equal(claims.family_name); expect(raw.picture).to.equal(claims.picture); expect(raw.granted_scopes.split(" ")).not.to.contain( - "https://www.googleapis.com/auth/userinfo.email" + "https://www.googleapis.com/auth/userinfo.email", ); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -212,7 +220,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { .send({ // No postBody, all params in requestUri below. requestUri: `http://localhost?providerId=google.com&id_token=${encodeURIComponent( - fakeIdToken + fakeIdToken, )}`, returnIdpCredential: true, }) @@ -449,7 +457,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { email_verified: false, name: "Foo", picture: "http://localhost/photo-from-idp.png", - }) + }), ); await authApi() @@ -457,7 +465,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { .query({ key: "fake-api-key" }) .send({ requestUri: `http://localhost?providerId=${providerId2}&id_token=${encodeURIComponent( - fakeIdToken + fakeIdToken, )}`, returnIdpCredential: true, }) @@ -516,7 +524,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { .then((res) => { expectStatusCode(400, res); expect(res.body.error.message).to.contain( - "INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential:" + "INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential:", ); }); }); @@ -575,7 +583,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { expect(res.body).to.have.property("refreshToken").that.is.a("string"); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -619,7 +627,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { expect(res.body).to.have.property("refreshToken").that.is.a("string"); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -823,7 +831,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { fakeClaims({ sub: "12345", email, - }) + }), ); await authApi() .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") @@ -850,7 +858,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { sub: "12345", email, email_verified: true, - }) + }), ); const newIdToken = await authApi() .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") @@ -885,7 +893,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { fakeClaims({ sub: "12345", email, - }) + }), ); await authApi() .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") @@ -901,26 +909,6 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { }); }); - it("should error if usageMode is passthrough", async () => { - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - postBody: `providerId=google.com&id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, - requestUri: "http://localhost", - returnIdpCredential: true, - returnSecureToken: true, - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - it("should error if auth is disabled", async () => { const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); @@ -975,7 +963,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { authApi(), "google.com", claims, - tenant.tenantId + tenant.tenantId, ); await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER, tenant.tenantId); const beforeSignIn = await getAccountInfoByLocalId(authApi(), localId, tenant.tenantId); @@ -1093,7 +1081,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { expect(res.body).not.to.have.property("photoUrl"); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -1137,7 +1125,7 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { expect(rawUserInfo).to.eql(attributeStatements); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -1146,4 +1134,322 @@ describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { .eql(attributeStatements); }); }); + + describe("when blocking functions are present", () => { + afterEach(() => { + expect(nock.isDone()).to.be.true; + nock.cleanAll(); + }); + + it("should update modifiable fields for new users for beforeCreate", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.isNewUser).to.equal(true); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [FAKE_GOOGLE_ACCOUNT.rawId], + email: [FAKE_GOOGLE_ACCOUNT.email], + }); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + }); + }); + + it("should update modifiable fields for new users for beforeSignIn", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.isNewUser).to.equal(true); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [FAKE_GOOGLE_ACCOUNT.rawId], + email: [FAKE_GOOGLE_ACCOUNT.email], + }); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("beforeSignIn fields should overwrite beforeCreate fields for new users", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims", + displayName: "oldDisplayName", + photoUrl: "oldPhotoUrl", + emailVerified: false, + customClaims: { customAttribute: "oldCustom" }, + }, + }) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.isNewUser).to.equal(true); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [FAKE_GOOGLE_ACCOUNT.rawId], + email: [FAKE_GOOGLE_ACCOUNT.email], + }); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("should update modifiable fields for existing users", async () => { + const user = await registerUser(authApi(), { + email: "foo@example.com", + password: "notasecret", + }); + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Foo", + }); + const fakeIdToken = JSON.stringify(claims); + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + idToken: user.idToken, + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(!!res.body.isNewUser).to.equal(false); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [claims.sub], + email: [user.email], + }); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("should disable user if set", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "disabled", + disabled: true, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + }); }); diff --git a/src/emulator/auth/index.ts b/src/emulator/auth/index.ts index 701436ed3a3..fd293b3e92a 100644 --- a/src/emulator/auth/index.ts +++ b/src/emulator/auth/index.ts @@ -7,11 +7,23 @@ import { EmulatorLogger } from "../emulatorLogger"; import { Emulators, EmulatorInstance, EmulatorInfo } from "../types"; import { createApp } from "./server"; import { FirebaseError } from "../../error"; +import { trackEmulator } from "../../track"; export interface AuthEmulatorArgs { projectId: string; port?: number; host?: string; + singleProjectMode?: SingleProjectMode; +} + +/** + * An enum that dictates the behavior when the project ID in the request doesn't match the + * defaultProjectId. + */ +export enum SingleProjectMode { + NO_WARNING, + WARNING, + ERROR, } export class AuthEmulator implements EmulatorInstance { @@ -21,7 +33,7 @@ export class AuthEmulator implements EmulatorInstance { async start(): Promise { const { host, port } = this.getInfo(); - const app = await createApp(this.args.projectId); + const app = await createApp(this.args.projectId, this.args.singleProjectMode); const server = app.listen(port, host); this.destroyServer = utils.createDestroyer(server); } @@ -35,7 +47,7 @@ export class AuthEmulator implements EmulatorInstance { } getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.AUTH); + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.AUTH); return { @@ -49,8 +61,17 @@ export class AuthEmulator implements EmulatorInstance { return Emulators.AUTH; } - async importData(authExportDir: string, projectId: string): Promise { - const logger = EmulatorLogger.forEmulator(Emulators.DATABASE); + async importData( + authExportDir: string, + projectId: string, + options: { initiatedBy: string }, + ): Promise { + void trackEmulator("emulator_import", { + initiated_by: options.initiatedBy, + emulator_name: Emulators.AUTH, + }); + + const logger = EmulatorLogger.forEmulator(Emulators.AUTH); const { host, port } = this.getInfo(); // TODO: In the future when we support import on demand, clear data first. @@ -63,7 +84,7 @@ export class AuthEmulator implements EmulatorInstance { await importFromFile( { method: "PATCH", - host, + host: utils.connectableHostname(host), port, path: `/emulator/v1/projects/${projectId}/config`, headers: { @@ -71,13 +92,13 @@ export class AuthEmulator implements EmulatorInstance { "Content-Type": "application/json", }, }, - configPath + configPath, ); } else { logger.logLabeled( "WARN", "auth", - `Skipped importing config because ${configPath} does not exist.` + `Skipped importing config because ${configPath} does not exist.`, ); } @@ -89,7 +110,7 @@ export class AuthEmulator implements EmulatorInstance { await importFromFile( { method: "POST", - host, + host: utils.connectableHostname(host), port, path: `/identitytoolkit.googleapis.com/v1/projects/${projectId}/accounts:batchCreate`, headers: { @@ -99,13 +120,13 @@ export class AuthEmulator implements EmulatorInstance { }, accountsPath, // Ignore the error when there are no users. No action needed. - { ignoreErrors: ["MISSING_USER_ACCOUNT"] } + { ignoreErrors: ["MISSING_USER_ACCOUNT"] }, ); } else { logger.logLabeled( "WARN", "auth", - `Skipped importing accounts because ${accountsPath} does not exist.` + `Skipped importing accounts because ${accountsPath} does not exist.`, ); } } @@ -122,14 +143,14 @@ function stat(path: fs.PathLike): Promise { } else { return resolve(stats); } - }) + }), ); } function importFromFile( reqOptions: http.RequestOptions, path: fs.PathLike, - options: { ignoreErrors?: string[] } = {} + options: { ignoreErrors?: string[] } = {}, ): Promise { const readStream = fs.createReadStream(path); @@ -158,7 +179,7 @@ function importFromFile( } } return reject( - new FirebaseError(`Received HTTP status code: ${response.statusCode}\n${data}`) + new FirebaseError(`Received HTTP status code: ${response.statusCode}\n${data}`), ); }); } diff --git a/src/emulator/auth/mfa.spec.ts b/src/emulator/auth/mfa.spec.ts new file mode 100644 index 00000000000..f35e58320be --- /dev/null +++ b/src/emulator/auth/mfa.spec.ts @@ -0,0 +1,655 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; +import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; +import { + BEFORE_SIGN_IN_PATH, + BEFORE_SIGN_IN_URL, + BLOCKING_FUNCTION_HOST, + DISPLAY_NAME, + enrollPhoneMfa, + expectStatusCode, + getAccountInfoByIdToken, + getAccountInfoByLocalId, + inspectVerificationCodes, + PHOTO_URL, + registerTenant, + registerUser, + signInWithEmailLink, + signInWithPassword, + signInWithPhoneNumber, + TEST_PHONE_NUMBER, + TEST_PHONE_NUMBER_2, + TEST_PHONE_NUMBER_OBFUSCATED, + updateAccountByLocalId, + updateConfig, +} from "./testing/helpers"; +import { MfaEnrollment } from "./types"; +import { FirebaseJwtPayload } from "./operations"; + +describeAuthEmulator("mfa enrollment", ({ authApi, getClock }) => { + it("should error if account does not have email verified", async () => { + const { idToken } = await registerUser(authApi(), { + email: "unverified@example.com", + password: "testing", + }); + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneEnrollmentInfo: { phoneNumber: TEST_PHONE_NUMBER } }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal( + "UNVERIFIED_EMAIL : Need to verify email first before enrolling second factors.", + ); + }); + }); + + it("should allow phone enrollment for an existing account", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneEnrollmentInfo: { phoneNumber } }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.phoneSessionInfo.sessionInfo).to.be.a("string"); + return res.body.phoneSessionInfo.sessionInfo as string; + }); + + const codes = await inspectVerificationCodes(authApi()); + expect(codes).to.have.length(1); + expect(codes[0].phoneNumber).to.equal(phoneNumber); + expect(codes[0].sessionInfo).to.equal(sessionInfo); + expect(codes[0].code).to.be.a("string"); + const { code } = codes[0]; + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneVerificationInfo: { code, sessionInfo } }); + + expectStatusCode(200, res); + expect(res.body.idToken).to.be.a("string"); + expect(res.body.refreshToken).to.be.a("string"); + + const userInfo = await getAccountInfoByIdToken(authApi(), idToken); + expect(userInfo.mfaInfo).to.be.an("array").with.lengthOf(1); + expect(userInfo.mfaInfo![0].phoneInfo).to.equal(phoneNumber); + const mfaEnrollmentId = userInfo.mfaInfo![0].mfaEnrollmentId; + + const decoded = decodeJwt(res.body.idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase.sign_in_second_factor).to.equal("phone"); + expect(decoded!.payload.firebase.second_factor_identifier).to.equal(mfaEnrollmentId); + }); + + it("should error if phoneEnrollmentInfo is not specified", async () => { + const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ idToken }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("INVALID_ARGUMENT"); + }); + }); + + it("should error if phoneNumber is invalid", async () => { + const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneEnrollmentInfo: { phoneNumber: "notaphonenumber" } }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("INVALID_PHONE_NUMBER"); + }); + }); + + it("should error if phoneNumber is a duplicate", async () => { + const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); + await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneEnrollmentInfo: { phoneNumber: TEST_PHONE_NUMBER } }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal( + "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account.", + ); + }); + }); + + it("should error if sign-in method of idToken is ineligible for MFA", async () => { + const { idToken, localId } = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); + await updateAccountByLocalId(authApi(), localId, { + email: "bob@example.com", + emailVerified: true, + }); + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneEnrollmentInfo: { phoneNumber: TEST_PHONE_NUMBER_2 } }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal( + "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor.", + ); + }); + }); + + it("should error on mfaEnrollment:start if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); + + it("should error on mfaEnrollment:start if MFA is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "DISABLED", + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should error on mfaEnrollment:start if phone SMS is not an enabled provider", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "ENABLED", + enabledProviders: ["PROVIDER_UNSPECIFIED"], + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should error on mfaEnrollment:finalize if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); + + it("should error on mfaEnrollment:finalize if MFA is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "DISABLED", + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should error on mfaEnrollment:finalize if phone SMS is not an enabled provider", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "ENABLED", + enabledProviders: ["PROVIDER_UNSPECIFIED"], + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should allow sign-in with pending credential for MFA-enabled user", async () => { + const email = "foo@example.com"; + const password = "abcdef"; + const { idToken, localId } = await registerUser(authApi(), { email, password }); + await updateAccountByLocalId(authApi(), localId, { emailVerified: true }); + await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); + const beforeSignIn = await getAccountInfoByLocalId(authApi(), localId); + + getClock().tick(3333); + + const { mfaPendingCredential, mfaEnrollmentId } = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email, password }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("idToken"); + expect(res.body).not.to.have.property("refreshToken"); + const mfaPendingCredential = res.body.mfaPendingCredential as string; + const mfaInfo = res.body.mfaInfo as MfaEnrollment[]; + expect(mfaPendingCredential).to.be.a("string"); + expect(mfaInfo).to.be.an("array").with.lengthOf(1); + expect(mfaInfo[0]?.phoneInfo).to.equal(TEST_PHONE_NUMBER_OBFUSCATED); + + // This must not be exposed right after first factor login. + expect(mfaInfo[0]?.phoneInfo).not.to.have.property("unobfuscatedPhoneInfo"); + return { mfaPendingCredential, mfaEnrollmentId: mfaInfo[0].mfaEnrollmentId }; + }); + + // Login / refresh timestamps should not change until MFA was successful. + const afterFirstFactor = await getAccountInfoByLocalId(authApi(), localId); + expect(afterFirstFactor.lastLoginAt).to.equal(beforeSignIn.lastLoginAt); + expect(afterFirstFactor.lastRefreshAt).to.equal(beforeSignIn.lastRefreshAt); + + getClock().tick(4444); + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") + .query({ key: "fake-api-key" }) + .send({ + mfaEnrollmentId, + mfaPendingCredential, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.phoneResponseInfo.sessionInfo).to.be.a("string"); + return res.body.phoneResponseInfo.sessionInfo as string; + }); + + const code = (await inspectVerificationCodes(authApi()))[0].code; + + getClock().tick(5555); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") + .query({ key: "fake-api-key" }) + .send({ + mfaPendingCredential, + phoneVerificationInfo: { + sessionInfo, + code: code, + }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.idToken).to.be.a("string"); + expect(res.body.refreshToken).to.be.a("string"); + + const decoded = decodeJwt(res.body.idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase.sign_in_second_factor).to.equal("phone"); + expect(decoded!.payload.firebase.second_factor_identifier).to.equal(mfaEnrollmentId); + }); + + // Login / refresh timestamps should now be updated. + const afterMfa = await getAccountInfoByLocalId(authApi(), localId); + expect(afterMfa.lastLoginAt).to.equal(Date.now().toString()); + expect(afterMfa.lastRefreshAt).to.equal(new Date().toISOString()); + }); + + it("should error on mfaSignIn:start if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); + + it("should error on mfaSignIn:start if MFA is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "DISABLED", + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should error on mfaSignIn:start if phone SMS is not an enabled provider", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "ENABLED", + enabledProviders: ["PROVIDER_UNSPECIFIED"], + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should error on mfaSignIn:finalize if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); + + it("should error on mfaSignIn:finalize if MFA is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "DISABLED", + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should error on mfaSignIn:finalize if phone SMS is not an enabled provider", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "ENABLED", + enabledProviders: ["PROVIDER_UNSPECIFIED"], + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should allow withdrawing MFA for a user", async () => { + const { idToken: token1 } = await signInWithEmailLink(authApi(), "foo@example.com"); + const { idToken } = await enrollPhoneMfa(authApi(), token1, TEST_PHONE_NUMBER); + + const { mfaInfo } = await getAccountInfoByIdToken(authApi(), idToken); + expect(mfaInfo).to.have.lengthOf(1); + const { mfaEnrollmentId } = mfaInfo![0]!; + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:withdraw") + .query({ key: "fake-api-key" }) + .send({ idToken, mfaEnrollmentId }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.idToken).to.be.a("string"); + expect(res.body.refreshToken).to.be.a("string"); + + const decoded = decodeJwt(res.body.idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase).not.to.have.property("sign_in_second_factor"); + expect(decoded!.payload.firebase).not.to.have.property("second_factor_identifier"); + }); + + const after = await getAccountInfoByIdToken(authApi(), idToken); + expect(after.mfaInfo).to.have.lengthOf(0); + }); + + it("should error on mfaEnrollment:withdraw if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:withdraw") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); + + describe("when blocking functions are present", () => { + afterEach(async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: {}, + }, + "blockingFunctions", + ); + expect(nock.isDone()).to.be.true; + nock.cleanAll(); + }); + + it("mfaSignIn:finalize should update modifiable fields before sign in", async () => { + const email = "foo@example.com"; + const password = "abcdef"; + const { idToken, localId } = await registerUser(authApi(), { email, password }); + await updateAccountByLocalId(authApi(), localId, { emailVerified: true }); + await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); + + getClock().tick(3333); + + const { mfaPendingCredential, mfaEnrollmentId } = await signInWithPassword( + authApi(), + email, + password, + true, + ); + + getClock().tick(4444); + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") + .query({ key: "fake-api-key" }) + .send({ + mfaEnrollmentId, + mfaPendingCredential, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.phoneResponseInfo.sessionInfo).to.be.a("string"); + return res.body.phoneResponseInfo.sessionInfo as string; + }); + + const code = (await inspectVerificationCodes(authApi()))[0].code; + + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + + getClock().tick(5555); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") + .query({ key: "fake-api-key" }) + .send({ + mfaPendingCredential, + phoneVerificationInfo: { + sessionInfo, + code: code, + }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.idToken).to.be.a("string"); + expect(res.body.refreshToken).to.be.a("string"); + + const decoded = decodeJwt(res.body.idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase.sign_in_second_factor).to.equal("phone"); + expect(decoded!.payload.firebase.second_factor_identifier).to.equal(mfaEnrollmentId); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("mfaSignIn:finalize should disable user if set", async () => { + const email = "foo@example.com"; + const password = "abcdef"; + const { idToken, localId } = await registerUser(authApi(), { email, password }); + await updateAccountByLocalId(authApi(), localId, { emailVerified: true }); + await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); + + getClock().tick(3333); + + const { mfaPendingCredential, mfaEnrollmentId } = await signInWithPassword( + authApi(), + email, + password, + true, + ); + + getClock().tick(4444); + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") + .query({ key: "fake-api-key" }) + .send({ + mfaEnrollmentId, + mfaPendingCredential, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.phoneResponseInfo.sessionInfo).to.be.a("string"); + return res.body.phoneResponseInfo.sessionInfo as string; + }); + + const code = (await inspectVerificationCodes(authApi()))[0].code; + + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "disabled", + disabled: true, + }, + }); + + getClock().tick(5555); + + await authApi() + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") + .query({ key: "fake-api-key" }) + .send({ + mfaPendingCredential, + phoneVerificationInfo: { + sessionInfo, + code: code, + }, + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); + }); + }); + }); +}); diff --git a/src/emulator/auth/misc.spec.ts b/src/emulator/auth/misc.spec.ts new file mode 100644 index 00000000000..58d697cfb94 --- /dev/null +++ b/src/emulator/auth/misc.spec.ts @@ -0,0 +1,664 @@ +import { expect } from "chai"; +import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; +import { decodeRefreshToken, encodeRefreshToken, RefreshTokenRecord, UserInfo } from "./state"; +import { + getAccountInfoByIdToken, + PROJECT_ID, + registerTenant, + signInWithPhoneNumber, + TEST_PHONE_NUMBER, +} from "./testing/helpers"; +import { describeAuthEmulator } from "./testing/setup"; +import { + deleteAccount, + expectStatusCode, + registerUser, + registerAnonUser, + updateAccountByLocalId, + expectUserNotExistsForIdToken, +} from "./testing/helpers"; +import { FirebaseJwtPayload, SESSION_COOKIE_MAX_VALID_DURATION } from "./operations"; +import { toUnixTimestamp } from "./utils"; +import { SingleProjectMode } from "."; + +describeAuthEmulator("token refresh", ({ authApi, getClock }) => { + it("should exchange refresh token for new tokens", async () => { + const { refreshToken, localId } = await registerAnonUser(authApi()); + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ refresh_token: refreshToken, grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.id_token).to.be.a("string"); + expect(res.body.access_token).to.equal(res.body.id_token); + expect(res.body.refresh_token).to.be.a("string"); + expect(res.body.expires_in) + .to.be.a("string") + .matches(/[0-9]+/); + expect(res.body.project_id).to.equal("12345"); + expect(res.body.token_type).to.equal("Bearer"); + expect(res.body.user_id).to.equal(localId); + }); + }); + + it("should exchange refresh tokens for new tokens in a tenant project", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + allowPasswordSignup: true, + }); + const { refreshToken, localId } = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + tenantId: tenant.tenantId, + }); + + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ refresh_token: refreshToken, grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.id_token).to.be.a("string"); + expect(res.body.access_token).to.equal(res.body.id_token); + expect(res.body.refresh_token).to.be.a("string"); + expect(res.body.expires_in) + .to.be.a("string") + .matches(/[0-9]+/); + expect(res.body.project_id).to.equal("12345"); + expect(res.body.token_type).to.equal("Bearer"); + expect(res.body.user_id).to.equal(localId); + + const refreshTokenRecord = decodeRefreshToken(res.body.refresh_token); + expect(refreshTokenRecord.tenantId).to.equal(tenant.tenantId); + }); + }); + + it("should populate auth_time to match lastLoginAt (in seconds since epoch)", async () => { + getClock().tick(444); // Make timestamps a bit more interesting (non-zero). + const emailUser = { email: "alice@example.com", password: "notasecret" }; + const { refreshToken } = await registerUser(authApi(), emailUser); + + getClock().tick(2000); // Wait 2 seconds before refreshing. + + const res = await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ refresh_token: refreshToken, grantType: "refresh_token" }) + .query({ key: "fake-api-key" }); + + const idToken = res.body.id_token; + const user = await getAccountInfoByIdToken(authApi(), idToken); + expect(user.lastLoginAt).not.to.be.undefined; + const lastLoginAtSeconds = Math.floor(parseInt(user.lastLoginAt!, 10) / 1000); + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + // This should match login time, not token refresh time. + expect(decoded!.payload.auth_time).to.equal(lastLoginAtSeconds); + }); + + it("should error if grant type is missing", async () => { + const { refreshToken } = await registerAnonUser(authApi()); + + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ refresh_token: refreshToken }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("MISSING_GRANT_TYPE"); + }); + }); + + it("should error if grant type is not refresh_token", async () => { + const { refreshToken } = await registerAnonUser(authApi()); + + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ refresh_token: refreshToken, grantType: "other_grant_type" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("INVALID_GRANT_TYPE"); + }); + }); + + it("should error if refresh token is missing", async () => { + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("MISSING_REFRESH_TOKEN"); + }); + }); + + it("should error on malformed refresh tokens", async () => { + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ refresh_token: "malformedToken", grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("INVALID_REFRESH_TOKEN"); + }); + }); + + it("should error if user is disabled", async () => { + const { refreshToken, localId } = await registerAnonUser(authApi()); + await updateAccountByLocalId(authApi(), localId, { disableUser: true }); + + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + .send({ refreshToken: refreshToken, grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + + it("should error when refresh tokens are from a different project", async () => { + const refreshTokenRecord = { + _AuthEmulatorRefreshToken: "DO NOT MODIFY", + localId: "localId", + provider: "provider", + extraClaims: {}, + projectId: "notMatchingProjectId", + }; + const refreshToken = encodeRefreshToken(refreshTokenRecord); + + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + .send({ refresh_token: refreshToken, grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("INVALID_REFRESH_TOKEN"); + }); + }); + + it("should error on refresh tokens without required fields", async () => { + const refreshTokenRecord = { + localId: "localId", + provider: "provider", + extraClaims: {}, + projectId: "notMatchingProjectId", + }; + const refreshToken = encodeRefreshToken(refreshTokenRecord as RefreshTokenRecord); + + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + .send({ refresh_token: refreshToken, grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("INVALID_REFRESH_TOKEN"); + }); + }); + + it("should error if the refresh token is for a user that does not exist", async () => { + const { refreshToken, idToken } = await registerAnonUser(authApi()); + await deleteAccount(authApi(), { idToken }); + + await authApi() + .post("/securetoken.googleapis.com/v1/token") + .type("form") + // snake_case parameters also work, per OAuth 2.0 spec. + .send({ refresh_token: refreshToken, grantType: "refresh_token" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("INVALID_REFRESH_TOKEN"); + }); + }); +}); + +describeAuthEmulator("createSessionCookie", ({ authApi }) => { + it("should return a valid sessionCookie", async () => { + const { idToken } = await registerAnonUser(authApi()); + const validDuration = 7777; /* seconds */ + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) + .set("Authorization", "Bearer owner") + .send({ idToken, validDuration: validDuration.toString() }) + .then((res) => { + expectStatusCode(200, res); + const sessionCookie = res.body.sessionCookie; + expect(sessionCookie).to.be.a("string"); + + const decoded = decodeJwt(sessionCookie, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "session cookie is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.iat).to.equal(toUnixTimestamp(new Date())); + expect(decoded!.payload.exp).to.equal(toUnixTimestamp(new Date()) + validDuration); + expect(decoded!.payload.iss).to.equal(`https://session.firebase.google.com/${PROJECT_ID}`); + + const idTokenProps = decodeJwt(idToken) as Partial; + delete idTokenProps.iss; + delete idTokenProps.iat; + delete idTokenProps.exp; + expect(decoded!.payload).to.deep.contain(idTokenProps); + }); + }); + + it("should throw if idToken is missing", async () => { + const validDuration = 7777; /* seconds */ + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) + .set("Authorization", "Bearer owner") + .send({ validDuration: validDuration.toString() /* no idToken */ }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("MISSING_ID_TOKEN"); + }); + }); + + it("should throw if idToken is invalid", async () => { + const validDuration = 7777; /* seconds */ + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) + .set("Authorization", "Bearer owner") + .send({ idToken: "invalid", validDuration: validDuration.toString() }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("INVALID_ID_TOKEN"); + }); + }); + + it("should use default session cookie validDuration if not specified", async () => { + const { idToken } = await registerAnonUser(authApi()); + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) + .set("Authorization", "Bearer owner") + .send({ idToken }) + .then((res) => { + expectStatusCode(200, res); + const sessionCookie = res.body.sessionCookie; + expect(sessionCookie).to.be.a("string"); + + const decoded = decodeJwt(sessionCookie, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "session cookie is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.exp).to.equal( + toUnixTimestamp(new Date()) + SESSION_COOKIE_MAX_VALID_DURATION, + ); + }); + }); + + it("should throw if validDuration is too short or too long", async () => { + const { idToken } = await registerAnonUser(authApi()); + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) + .set("Authorization", "Bearer owner") + .send({ idToken, validDuration: "1" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("INVALID_DURATION"); + }); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) + .set("Authorization", "Bearer owner") + .send({ idToken, validDuration: "999999999999" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("INVALID_DURATION"); + }); + }); +}); + +describeAuthEmulator("accounts:lookup", ({ authApi }) => { + it("should return user by localId when privileged", async () => { + const { localId } = await registerAnonUser(authApi()); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .set("Authorization", "Bearer owner") + .send({ localId: [localId] }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.users).to.have.length(1); + expect(res.body.users[0].localId).to.equal(localId); + }); + }); + + it("should deduplicate users", async () => { + const { localId } = await registerAnonUser(authApi()); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .set("Authorization", "Bearer owner") + .send({ localId: [localId, localId] /* two with the same id */ }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.users).to.have.length(1); + expect(res.body.users[0].localId).to.equal(localId); + }); + }); + + it("should return providerUserInfo for phone auth users", async () => { + const { localId } = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .set("Authorization", "Bearer owner") + .send({ localId: [localId] }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.users).to.have.length(1); + expect(res.body.users[0].providerUserInfo).to.eql([ + { + phoneNumber: TEST_PHONE_NUMBER, + rawId: TEST_PHONE_NUMBER, + providerId: "phone", + }, + ]); + }); + }); + + it("should return empty result when localId is not found", async () => { + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .set("Authorization", "Bearer owner") + .send({ localId: ["noSuchId"] }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("users"); + }); + }); + + it("should return user by tenantId in idToken", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + allowPasswordSignup: true, + }); + const { idToken, localId } = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + tenantId: tenant.tenantId, + }); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/accounts:lookup`) + .send({ idToken }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.users).to.have.length(1); + expect(res.body.users[0].localId).to.equal(localId); + }); + }); + + it("should error if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") + .set("Authorization", "Bearer owner") + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").includes("PROJECT_DISABLED"); + }); + }); +}); + +describeAuthEmulator("accounts:query", ({ authApi }) => { + it("should return count of accounts when returnUserInfo is false", async () => { + await registerAnonUser(authApi()); + await registerAnonUser(authApi()); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:query`) + .set("Authorization", "Bearer owner") + .send({ returnUserInfo: false }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.recordsCount).to.equal("2"); // string (int64 format) + expect(res.body).not.to.have.property("userInfo"); + }); + }); + + it("should return accounts when returnUserInfo is true", async () => { + const { localId } = await registerAnonUser(authApi()); + const user = { email: "alice@example.com", password: "notasecret" }; + const { localId: localId2 } = await registerUser(authApi(), user); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:query`) + .set("Authorization", "Bearer owner") + .send({ + /* returnUserInfo is true by default */ + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.recordsCount).to.equal("2"); // string (int64 format) + expect(res.body.userInfo).to.be.an.instanceof(Array).with.lengthOf(2); + + const users = res.body.userInfo as UserInfo[]; + expect(users[0].localId < users[1].localId, "users are not sorted by ID ASC").to.be.true; + const anonUser = users.find((x) => x.localId === localId); + expect(anonUser, "cannot find first registered user").to.be.not.undefined; + + const emailUser = users.find((x) => x.localId === localId2); + expect(emailUser, "cannot find second registered user").to.be.not.undefined; + expect(emailUser!.email).to.equal(user.email); + }); + }); + + it("should error if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:query`) + .set("Authorization", "Bearer owner") + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); +}); + +describeAuthEmulator("emulator utility APIs", ({ authApi }) => { + it("should drop all accounts on DELETE /emulator/v1/projects/{PROJECT_ID}/accounts", async () => { + const user1 = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + }); + const user2 = await registerUser(authApi(), { + email: "bob@example.com", + password: "notasecret2", + }); + await authApi() + .delete(`/emulator/v1/projects/${PROJECT_ID}/accounts`) + .send() + .then((res) => expectStatusCode(200, res)); + + await expectUserNotExistsForIdToken(authApi(), user1.idToken); + await expectUserNotExistsForIdToken(authApi(), user2.idToken); + }); + + it("should drop all accounts on DELETE /emulator/v1/projects/{PROJECT_ID}/tenants/{TENANT_ID}/accounts", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + allowPasswordSignup: true, + }); + const user1 = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + tenantId: tenant.tenantId, + }); + const user2 = await registerUser(authApi(), { + email: "bob@example.com", + password: "notasecret2", + tenantId: tenant.tenantId, + }); + + await authApi() + .delete(`/emulator/v1/projects/${PROJECT_ID}/tenants/${tenant.tenantId}/accounts`) + .send() + .then((res) => expectStatusCode(200, res)); + + await expectUserNotExistsForIdToken(authApi(), user1.idToken, tenant.tenantId); + await expectUserNotExistsForIdToken(authApi(), user2.idToken, tenant.tenantId); + }); + + it("should return config on GET /emulator/v1/projects/{PROJECT_ID}/config", async () => { + await authApi() + .get(`/emulator/v1/projects/${PROJECT_ID}/config`) + .send() + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("signIn").eql({ + allowDuplicateEmails: false /* default value */, + }); + expect(res.body).to.have.property("emailPrivacyConfig").eql({ + enableImprovedEmailPrivacy: false /* default value */, + }); + }); + }); + + it("should not throw an exception on project ID mismatch if singleProjectMode is NO_WARNING", async () => { + await authApi() + .get(`/emulator/v1/projects/someproject/config`) // note the "wrong" project ID here + .send() + .then((res) => { + expectStatusCode(200, res); + }); + }); + + it("should only update allowDuplicateEmails on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { + await authApi() + .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) + .send({ signIn: { allowDuplicateEmails: true } }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("signIn").eql({ + allowDuplicateEmails: true, + }); + expect(res.body).to.have.property("emailPrivacyConfig").eql({ + enableImprovedEmailPrivacy: false, + }); + }); + await authApi() + .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) + .send({ signIn: { allowDuplicateEmails: false } }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("signIn").eql({ + allowDuplicateEmails: false, + }); + expect(res.body).to.have.property("emailPrivacyConfig").eql({ + enableImprovedEmailPrivacy: false, + }); + }); + }); + + it("should only update enableImprovedEmailPrivacy on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { + await authApi() + .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) + .send({ emailPrivacyConfig: { enableImprovedEmailPrivacy: true } }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("signIn").eql({ + allowDuplicateEmails: false, + }); + expect(res.body).to.have.property("emailPrivacyConfig").eql({ + enableImprovedEmailPrivacy: true, + }); + }); + await authApi() + .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) + .send({ emailPrivacyConfig: { enableImprovedEmailPrivacy: false } }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("signIn").eql({ + allowDuplicateEmails: false, + }); + expect(res.body).to.have.property("emailPrivacyConfig").eql({ + enableImprovedEmailPrivacy: false, + }); + }); + }); + + it("should update both allowDuplicateEmails and enableImprovedEmailPrivacy on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { + await authApi() + .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) + .send({ + signIn: { allowDuplicateEmails: true }, + emailPrivacyConfig: { enableImprovedEmailPrivacy: true }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("signIn").eql({ + allowDuplicateEmails: true, + }); + expect(res.body).to.have.property("emailPrivacyConfig").eql({ + enableImprovedEmailPrivacy: true, + }); + }); + await authApi() + .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) + .send({ + signIn: { allowDuplicateEmails: false }, + emailPrivacyConfig: { enableImprovedEmailPrivacy: false }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("signIn").eql({ + allowDuplicateEmails: false, + }); + expect(res.body).to.have.property("emailPrivacyConfig").eql({ + enableImprovedEmailPrivacy: false, + }); + }); + }); +}); + +describeAuthEmulator( + "emulator utility API; singleProjectMode=ERROR", + ({ authApi }) => { + it("should throw an exception on project ID mismatch if singleProjectMode is ERROR", async () => { + await authApi() + .get(`/emulator/v1/projects/someproject/config`) // note the "wrong" project ID here + .send() + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("single project mode"); + }); + }); + }, + SingleProjectMode.ERROR, +); diff --git a/src/test/emulators/auth/oob.spec.ts b/src/emulator/auth/oob.spec.ts similarity index 90% rename from src/test/emulators/auth/oob.spec.ts rename to src/emulator/auth/oob.spec.ts index 1da0ac50cc1..9ceaab98e6a 100644 --- a/src/test/emulators/auth/oob.spec.ts +++ b/src/emulator/auth/oob.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; import { expectStatusCode, registerUser, @@ -7,10 +7,9 @@ import { updateAccountByLocalId, expectIdTokenExpired, inspectOobs, - updateProjectConfig, - deleteAccount, registerTenant, -} from "./helpers"; + updateConfig, +} from "./testing/helpers"; describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => { it("should generate OOB code for verify email", async () => { @@ -211,24 +210,6 @@ describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => { expect(oobs).to.have.length(0); }); - it("should error if usageMode is passthrough", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { idToken } = await registerUser(authApi(), user); - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ idToken, requestType: "VERIFY_EMAIL" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - it("should error if auth is disabled", async () => { const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); @@ -349,33 +330,6 @@ describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => { expect(oobs2).to.have.length(3); }); - it("should error on resetPassword endpoint if usageMode is passthrough", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { idToken } = await registerUser(authApi(), user); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ requestType: "PASSWORD_RESET", email: user.email }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.email).to.equal(user.email); - }); - const oobs = await inspectOobs(authApi()); - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:resetPassword") - .query({ key: "fake-api-key" }) - .send({ oobCode: oobs[0].oobCode, newPassword: "notasecret2" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - it("should error on resetPassword if auth is disabled", async () => { const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); @@ -404,4 +358,41 @@ describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => { expect(res.body.error).to.have.property("message").equals("PASSWORD_LOGIN_DISABLED"); }); }); + + it("should error when sending a password reset to non-existent user with improved email privacy disabled", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ email: user.email, requestType: "PASSWORD_RESET", returnOobLink: true }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("EMAIL_NOT_FOUND"); + }); + }); + + it("should return email address when sending a password reset to non-existent user with improved email privacy enabled", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await updateConfig( + authApi(), + PROJECT_ID, + { + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }, + "emailPrivacyConfig", + ); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ email: user.email, requestType: "PASSWORD_RESET", returnOobLink: true }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body) + .to.have.property("kind") + .equals("identitytoolkit#GetOobConfirmationCodeResponse"); + expect(res.body).to.have.property("email").equals(user.email); + }); + }); }); diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts index 79b3fd3e06f..9b1b84b684f 100644 --- a/src/emulator/auth/operations.ts +++ b/src/emulator/auth/operations.ts @@ -1,6 +1,8 @@ import { URLSearchParams } from "url"; import { decode as decodeJwt, sign as signJwt, JwtHeader } from "jsonwebtoken"; import * as express from "express"; +import fetch from "node-fetch"; +import AbortController from "abort-controller"; import { ExegesisContext } from "exegesis-express"; import { toUnixTimestamp, @@ -12,6 +14,7 @@ import { authEmulatorUrl, MakeRequired, isValidPhoneNumber, + randomBase64UrlStr, } from "./utils"; import { NotImplementedError, assert, BadRequestError, InternalError } from "./errors"; import { Emulators } from "../types"; @@ -29,10 +32,10 @@ import { OobRecord, PROVIDER_GAME_CENTER, SecondFactorRecord, - UsageMode, AgentProjectState, TenantProjectState, MfaConfig, + BlockingFunctionEvents, } from "./state"; import { MfaEnrollments, Schemas } from "./types"; @@ -73,6 +76,8 @@ export const authOperations: AuthOps = { projects: { createSessionCookie, queryAccounts, + getConfig, + updateConfig, accounts: { _: signUp, delete: deleteAccount, @@ -142,16 +147,16 @@ const MFA_INELIGIBLE_PROVIDER = new Set([ PROVIDER_GAME_CENTER, ]); -function signUp( +async function signUp( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignUpRequest"], - ctx: ExegesisContext -): Schemas["GoogleCloudIdentitytoolkitV1SignUpResponse"] { + ctx: ExegesisContext, +): Promise { assert(!state.disableAuth, "PROJECT_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); let provider: string | undefined; - const updates: Omit, "localId" | "providerUserInfo"> = { - lastLoginAt: Date.now().toString(), + const timestamp = new Date(); + let updates: Omit, "localId" | "providerUserInfo"> = { + lastLoginAt: timestamp.getTime().toString(), }; if (ctx.security?.Oauth2) { @@ -193,7 +198,9 @@ function signUp( } } - if (reqBody.email) { + // Assert a valid email address when we expect the email to have a value. + // Prevents empty email and password string to be treated as anonymous sign in. + if (reqBody.email || (reqBody.email === "" && provider)) { assert(isValidEmailAddress(reqBody.email), "INVALID_EMAIL"); const email = canonicalizeEmailAddress(reqBody.email); assert(!state.getUserByEmail(email), "EMAIL_EXISTS"); @@ -202,7 +209,7 @@ function signUp( if (reqBody.password) { assert( reqBody.password.length >= PASSWORD_MIN_LENGTH, - `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters` + `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`, ); updates.salt = "fakeSalt" + randomId(20); updates.passwordHash = hashPassword(reqBody.password, updates.salt); @@ -222,12 +229,39 @@ function signUp( ({ user } = parseIdToken(state, reqBody.idToken)); } + let extraClaims; if (!user) { - if (reqBody.localId) { - user = state.createUserWithLocalId(reqBody.localId, updates); - assert(user, "DUPLICATE_LOCAL_ID"); - } else { - user = state.createUser(updates); + updates.createdAt = timestamp.getTime().toString(); + const localId = reqBody.localId ?? state.generateLocalId(); + if (reqBody.email && !ctx.security?.Oauth2) { + const userBeforeCreate = { localId, ...updates }; + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_CREATE, + userBeforeCreate, + { signInMethod: "password" }, + ); + updates = { ...updates, ...blockingResponse.updates }; + } + + user = state.createUserWithLocalId(localId, updates); + assert(user, "DUPLICATE_LOCAL_ID"); + + if (reqBody.email && !ctx.security?.Oauth2) { + if (!user.disabled) { + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + user, + { signInMethod: "password" }, + ); + updates = blockingResponse.updates; + extraClaims = blockingResponse.extraClaims; + user = state.updateUserByLocalId(user.localId, updates); + } + // User may have been disabled after either blocking function, but + // only throw after writing user to store + assert(!user.disabled, "USER_DISABLED"); } } else { user = state.updateUserByLocalId(user.localId, updates); @@ -238,14 +272,14 @@ function signUp( localId: user.localId, displayName: user.displayName, email: user.email, - ...(provider ? issueTokens(state, user, provider) : {}), + ...(provider ? issueTokens(state, user, provider, { extraClaims }) : {}), }; } function lookup( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1GetAccountInfoRequest"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1GetAccountInfoResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); const seenLocalIds = new Set(); @@ -293,10 +327,9 @@ function lookup( function batchCreate( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1UploadAccountRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1UploadAccountRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1UploadAccountResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); assert(reqBody.users?.length, "MISSING_USER_ACCOUNT"); if (reqBody.sanityCheck) { @@ -317,7 +350,7 @@ function batchCreate( const key = `${providerId}:${rawId}`; assert( !existingProviderAccounts.has(key), - `DUPLICATE_RAW_ID : Provider id(${providerId}), Raw id(${rawId})` + `DUPLICATE_RAW_ID : Provider id(${providerId}), Raw id(${rawId})`, ); existingProviderAccounts.add(key); } @@ -348,7 +381,7 @@ function batchCreate( if (userInfo.tenantId) { assert( state instanceof TenantProjectState && state.tenantId === userInfo.tenantId, - "Tenant id in userInfo does not match the tenant id in request." + "Tenant id in userInfo does not match the tenant id in request.", ); } if (state instanceof TenantProjectState) { @@ -389,14 +422,14 @@ function batchCreate( // TODO assert( false, - "((Parsing federatedId is not implemented in Auth Emulator; please specify providerId AND rawId as a workaround.))" + "((Parsing federatedId is not implemented in Auth Emulator; please specify providerId AND rawId as a workaround.))", ); } } const existingUserWithRawId = state.getUserByProviderRawId(providerId, rawId); assert( !existingUserWithRawId || existingUserWithRawId.localId === userInfo.localId, - "raw id exists in other account in database" + "raw id exists in other account in database", ); fields.providerUserInfo.push({ ...providerUserInfo, providerId, rawId }); } @@ -425,7 +458,7 @@ function batchCreate( !existingUserWithEmail || existingUserWithEmail.localId === userInfo.localId, reqBody.sanityCheck && state.oneAccountPerEmail ? "email exists in other account in database" - : `((Auth Emulator does not support importing duplicate email: ${email}))` + : `((Auth Emulator does not support importing duplicate email: ${email}))`, ); fields.email = canonicalizeEmailAddress(email); } @@ -433,7 +466,7 @@ function batchCreate( fields.disabled = !!userInfo.disabled; // MFA - if (userInfo.mfaInfo) { + if (userInfo.mfaInfo && userInfo.mfaInfo.length > 0) { fields.mfaInfo = []; assert(fields.email, "Second factor account requires email to be presented."); assert(fields.emailVerified, "Second factor account requires email to be verified."); @@ -458,11 +491,11 @@ function batchCreate( if (state.getUserByLocalId(userInfo.localId)) { assert( reqBody.allowOverwrite, - "localId belongs to an existing account - can not overwrite." + "localId belongs to an existing account - can not overwrite.", ); } state.overwriteUserWithLocalId(userInfo.localId, fields); - } catch (e) { + } catch (e: any) { if (e instanceof BadRequestError) { // Use friendlier messages for some codes, consistent with production. let message = e.message; @@ -490,7 +523,7 @@ function batchCreate( function batchDelete( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse"] { const errors: Required< Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse"]["errors"] @@ -520,14 +553,14 @@ function batchDelete( function batchGet( state: ProjectState, reqBody: unknown, - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1DownloadAccountResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); const maxResults = Math.min(Math.floor(ctx.params.query.maxResults) || 20, 1000); const users = state.queryUsers( {}, - { sortByField: "localId", order: "ASC", startToken: ctx.params.query.nextPageToken } + { sortByField: "localId", order: "ASC", startToken: ctx.params.query.nextPageToken }, ); let newPageToken: string | undefined = undefined; @@ -548,10 +581,9 @@ function batchGet( function createAuthUri( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1CreateAuthUriRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1CreateAuthUriRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1CreateAuthUriResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); const sessionId = reqBody.sessionId || randomId(27); if (reqBody.providerId) { throw new NotImplementedError("Sign-in with IDP is not yet supported."); @@ -606,13 +638,20 @@ function createAuthUri( } } - return { - kind: "identitytoolkit#CreateAuthUriResponse", - registered, - allProviders, - sessionId, - signinMethods, - }; + if (state.enableImprovedEmailPrivacy) { + return { + kind: "identitytoolkit#CreateAuthUriResponse", + sessionId, + }; + } else { + return { + kind: "identitytoolkit#CreateAuthUriResponse", + registered, + allProviders, + sessionId, + signinMethods, + }; + } } const SESSION_COOKIE_MIN_VALID_DURATION = 5 * 60; /* 5 minutes in seconds */ @@ -621,15 +660,13 @@ export const SESSION_COOKIE_MAX_VALID_DURATION = 14 * 24 * 60 * 60; /* 14 days i function createSessionCookie( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest"], - ctx: ExegesisContext ): Schemas["GoogleCloudIdentitytoolkitV1CreateSessionCookieResponse"] { - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); assert(reqBody.idToken, "MISSING_ID_TOKEN"); const validDuration = Number(reqBody.validDuration) || SESSION_COOKIE_MAX_VALID_DURATION; assert( validDuration >= SESSION_COOKIE_MIN_VALID_DURATION && validDuration <= SESSION_COOKIE_MAX_VALID_DURATION, - "INVALID_DURATION" + "INVALID_DURATION", ); const { payload } = parseIdToken(state, reqBody.idToken); const issuedAt = toUnixTimestamp(new Date()); @@ -641,12 +678,12 @@ function createSessionCookie( exp: expiresAt, iss: `https://session.firebase.google.com/${payload.aud}`, }, - "", + "fake-secret", { // Generate a unsigned (insecure) JWT. Admin SDKs should treat this like // a real token (if in emulator mode). This won't work in production. algorithm: "none", - } + }, ); return { sessionCookie }; @@ -655,7 +692,7 @@ function createSessionCookie( function deleteAccount( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1DeleteAccountRequest"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1DeleteAccountResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); let user: UserInfo; @@ -677,7 +714,7 @@ function deleteAccount( } function getProjects( - state: ProjectState + state: ProjectState, ): Schemas["GoogleCloudIdentitytoolkitV1GetProjectConfigResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(state instanceof AgentProjectState, "UNSUPPORTED_TENANT_OPERATION"); @@ -693,7 +730,7 @@ function getProjects( } function getRecaptchaParams( - state: ProjectState + state: ProjectState, ): Schemas["GoogleCloudIdentitytoolkitV1GetRecaptchaParamResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); return { @@ -712,7 +749,7 @@ function getRecaptchaParams( function queryAccounts( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1QueryUserInfoResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); if (reqBody.expression?.length) { @@ -771,10 +808,9 @@ function queryAccounts( */ export function resetPassword( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1ResetPasswordRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1ResetPasswordRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1ResetPasswordResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); assert(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED"); assert(reqBody.oobCode, "MISSING_OOB_CODE"); const oob = state.validateOobCode(reqBody.oobCode); @@ -784,7 +820,7 @@ export function resetPassword( assert(oob.requestType === "PASSWORD_RESET", "INVALID_OOB_CODE"); assert( reqBody.newPassword.length >= PASSWORD_MIN_LENGTH, - `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters` + `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`, ); state.deleteOobCode(reqBody.oobCode); let user = state.getUserByEmail(oob.email); @@ -801,7 +837,7 @@ export function resetPassword( passwordUpdatedAt: Date.now(), validSince: toUnixTimestamp(new Date()).toString(), }, - { deleteProviders: user.providerUserInfo?.map((info) => info.providerId) } + { deleteProviders: user.providerUserInfo?.map((info) => info.providerId) }, ); } @@ -819,13 +855,12 @@ export function resetPassword( function sendOobCode( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1GetOobCodeRequest"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1GetOobCodeResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); assert( reqBody.requestType && reqBody.requestType !== "OOB_REQ_TYPE_UNSPECIFIED", - "MISSING_REQ_TYPE" + "MISSING_REQ_TYPE", ); if (reqBody.returnOobLink) { assert(ctx.security?.Oauth2, "INSUFFICIENT_PERMISSION"); @@ -833,7 +868,7 @@ function sendOobCode( if (reqBody.continueUrl) { assert( parseAbsoluteUri(reqBody.continueUrl), - "INVALID_CONTINUE_URI: ((expected an absolute URI with valid scheme and host))" + "INVALID_CONTINUE_URI : ((expected an absolute URI with valid scheme and host))", ); } @@ -851,7 +886,14 @@ function sendOobCode( mode = "resetPassword"; assert(reqBody.email, "MISSING_EMAIL"); email = canonicalizeEmailAddress(reqBody.email); - assert(state.getUserByEmail(email), "EMAIL_NOT_FOUND"); + const maybeUser = state.getUserByEmail(email); + if (state.enableImprovedEmailPrivacy && !maybeUser) { + return { + kind: "identitytoolkit#GetOobConfirmationCodeResponse", + email, + }; + } + assert(maybeUser, "EMAIL_NOT_FOUND"); break; case "VERIFY_EMAIL": mode = "verifyEmail"; @@ -870,7 +912,7 @@ function sendOobCode( email = user.email; } break; - + // TODO: implement case for requestType VERIFY_AND_CHANGE_EMAIL. default: throw new NotImplementedError(reqBody.requestType); } @@ -878,7 +920,7 @@ function sendOobCode( if (reqBody.canHandleCodeInApp) { EmulatorLogger.forEmulator(Emulators.AUTH).log( "WARN", - "canHandleCodeInApp is unsupported in Auth Emulator. All OOB operations will complete via web." + "canHandleCodeInApp is unsupported in Auth Emulator. All OOB operations will complete via web.", ); } @@ -908,24 +950,23 @@ function sendOobCode( function sendVerificationCode( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1SendVerificationCodeRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1SendVerificationCodeRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1SendVerificationCodeResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(state instanceof AgentProjectState, "UNSUPPORTED_TENANT_OPERATION"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); // reqBody.iosReceipt, iosSecret, and recaptchaToken are intentionally ignored. // Production Firebase Auth service also throws INVALID_PHONE_NUMBER instead // of MISSING_XXXX when phoneNumber is missing. Matching the behavior here. assert( reqBody.phoneNumber && isValidPhoneNumber(reqBody.phoneNumber), - "INVALID_PHONE_NUMBER : Invalid format." + "INVALID_PHONE_NUMBER : Invalid format.", ); const user = state.getUserByPhoneNumber(reqBody.phoneNumber); assert( !user?.mfaInfo?.length, - "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user." + "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user.", ); const { sessionInfo, phoneNumber, code } = state.createVerificationCode(reqBody.phoneNumber); @@ -934,7 +975,7 @@ function sendVerificationCode( // a real text message out to the phone number. EmulatorLogger.forEmulator(Emulators.AUTH).log( "BULLET", - `To verify the phone number ${phoneNumber}, use the code ${code}.` + `To verify the phone number ${phoneNumber}, use the code ${code}.`, ); return { @@ -945,10 +986,9 @@ function sendVerificationCode( function setAccountInfo( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); const url = authEmulatorUrl(ctx.req as express.Request); return setAccountInfoImpl(state, reqBody, { privileged: !!ctx.security?.Oauth2, @@ -968,7 +1008,7 @@ function setAccountInfo( export function setAccountInfoImpl( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"], - { privileged = false, emulatorUrl = undefined }: { privileged?: boolean; emulatorUrl?: URL } = {} + { privileged = false, emulatorUrl = undefined }: { privileged?: boolean; emulatorUrl?: URL } = {}, ): Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"] { // TODO: Implement these. const unimplementedFields: (keyof typeof reqBody)[] = [ @@ -985,7 +1025,7 @@ export function setAccountInfoImpl( if (!privileged) { assert( reqBody.idToken || reqBody.oobCode, - "INVALID_REQ_TYPE : Unsupported request parameters." + "INVALID_REQ_TYPE : Unsupported request parameters.", ); assert(reqBody.customAttributes == null, "INSUFFICIENT_PERMISSION"); } else { @@ -1074,7 +1114,7 @@ export function setAccountInfoImpl( if (reqBody.password) { assert( reqBody.password.length >= PASSWORD_MIN_LENGTH, - `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters` + `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`, ); updates.salt = "fakeSalt" + randomId(20); updates.passwordHash = hashPassword(reqBody.password, updates.salt); @@ -1116,7 +1156,7 @@ export function setAccountInfoImpl( "customAttributes", "createdAt", "lastLoginAt", - "validSince" + "validSince", ); } for (const field of fieldsToCopy) { @@ -1204,7 +1244,7 @@ function createOobRecord( requestType: OobRequestType; mode: string; continueUrl?: string; - } + }, ): OobRecord { const oobRecord = state.createOob(email, params.requestType, (oobCode) => { url.pathname = "/emulator/action"; @@ -1260,12 +1300,11 @@ function logOobMessage(oobRecord: OobRecord) { function signInWithCustomToken( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithCustomTokenRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithCustomTokenRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1SignInWithCustomTokenResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(reqBody.token, "MISSING_CUSTOM_TOKEN"); - // eslint-disable-next-line camelcase let payload: { aud?: unknown; uid?: unknown; @@ -1280,12 +1319,12 @@ function signInWithCustomToken( payload = JSON.parse(reqBody.token); } catch { throw new BadRequestError( - "INVALID_CUSTOM_TOKEN : ((Auth Emulator only accepts strict JSON or JWTs as fake custom tokens.))" + "INVALID_CUSTOM_TOKEN : ((Auth Emulator only accepts strict JSON or JWTs as fake custom tokens.))", ); } // Don't check payload.aud for JSON strings, making them easier to construct. } else { - const decoded = decodeJwt(reqBody.token, { complete: true }) as { + const decoded = decodeJwt(reqBody.token, { complete: true }) as unknown as { header: JwtHeader; payload: typeof payload; } | null; @@ -1300,13 +1339,13 @@ function signInWithCustomToken( // valid with a warning. EmulatorLogger.forEmulator(Emulators.AUTH).log( "WARN", - "Received a signed custom token. Auth Emulator does not validate JWTs and IS NOT SECURE" + "Received a signed custom token. Auth Emulator does not validate JWTs and IS NOT SECURE", ); } assert( decoded.payload.aud === CUSTOM_TOKEN_AUDIENCE, `INVALID_CUSTOM_TOKEN : ((Invalid aud (audience): ${decoded.payload.aud} ` + - "Note: Firebase ID Tokens / third-party tokens cannot be used with signInWithCustomToken.))" + "Note: Firebase ID Tokens / third-party tokens cannot be used with signInWithCustomToken.))", ); // We do not verify iss or sub since these are service account emails that // we cannot reasonably validate within the emulator. @@ -1324,11 +1363,12 @@ function signInWithCustomToken( } let user = state.getUserByLocalId(localId); - const isNewUser = state.usageMode === UsageMode.PASSTHROUGH ? false : !user; + const isNewUser = !user; - const updates = { + const timestamp = new Date(); + const updates: Partial = { customAuth: true, - lastLoginAt: Date.now().toString(), + lastLoginAt: timestamp.getTime().toString(), tenantId: state instanceof TenantProjectState ? state.tenantId : undefined, }; @@ -1336,6 +1376,7 @@ function signInWithCustomToken( assert(!user.disabled, "USER_DISABLED"); user = state.updateUserByLocalId(localId, updates); } else { + updates.createdAt = timestamp.getTime().toString(); user = state.createUserWithLocalId(localId, updates); if (!user) { throw new Error(`Internal assertion error: trying to create duplicate localId: ${localId}`); @@ -1349,13 +1390,12 @@ function signInWithCustomToken( }; } -function signInWithEmailLink( +async function signInWithEmailLink( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithEmailLinkRequest"] -): Schemas["GoogleCloudIdentitytoolkitV1SignInWithEmailLinkResponse"] { + reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithEmailLinkRequest"], +): Promise { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(state.enableEmailLinkSignin, "OPERATION_NOT_ALLOWED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined; assert(reqBody.email, "MISSING_EMAIL"); const email = canonicalizeEmailAddress(reqBody.email); @@ -1364,12 +1404,16 @@ function signInWithEmailLink( assert(oob && oob.requestType === "EMAIL_SIGNIN", "INVALID_OOB_CODE"); assert( email === oob.email, - "INVALID_EMAIL : The email provided does not match the sign-in email address." + "INVALID_EMAIL : The email provided does not match the sign-in email address.", ); - state.deleteOobCode(reqBody.oobCode); - const updates: Omit, "localId" | "providerUserInfo"> = { + const userFromEmail = state.getUserByEmail(email); + let user = userFromIdToken || userFromEmail; + const isNewUser = !user; + + const timestamp = new Date(); + let updates: Omit, "localId" | "providerUserInfo"> = { email, emailVerified: true, emailLinkSignin: true, @@ -1379,17 +1423,49 @@ function signInWithEmailLink( updates.tenantId = state.tenantId; } - let user = state.getUserByEmail(email); - const isNewUser = !user && !userFromIdToken; + let extraClaims; if (!user) { - if (userFromIdToken) { - user = state.updateUserByLocalId(userFromIdToken.localId, updates); - } else { - user = state.createUser(updates); + updates.createdAt = timestamp.getTime().toString(); + const localId = state.generateLocalId(); + const userBeforeCreate = { localId, ...updates }; + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_CREATE, + userBeforeCreate, + { signInMethod: "emailLink" }, + ); + + updates = { ...updates, ...blockingResponse.updates }; + user = state.createUserWithLocalId(localId, updates)!; + + if (!user.disabled && !isMfaEnabled(state, user)) { + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + user, + { signInMethod: "emailLink" }, + ); + updates = blockingResponse.updates; + extraClaims = blockingResponse.extraClaims; + user = state.updateUserByLocalId(user.localId, updates); } } else { assert(!user.disabled, "USER_DISABLED"); - assert(!userFromIdToken || userFromIdToken.localId === user.localId, "EMAIL_EXISTS"); + if (userFromIdToken && userFromEmail) { + assert(userFromIdToken.localId === userFromEmail.localId, "EMAIL_EXISTS"); + } + + if (!user.disabled && !isMfaEnabled(state, user)) { + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + { ...user, ...updates }, + { signInMethod: "emailLink" }, + ); + updates = { ...updates, ...blockingResponse.updates }; + extraClaims = blockingResponse.extraClaims; + } + user = state.updateUserByLocalId(user.localId, updates); } @@ -1400,25 +1476,24 @@ function signInWithEmailLink( isNewUser, }; - if ( - (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && - user.mfaInfo?.length - ) { + // User may have been disabled but only throw after writing user to store + assert(!user.disabled, "USER_DISABLED"); + + if (isMfaEnabled(state, user)) { return { ...response, ...mfaPending(state, user, PROVIDER_PASSWORD) }; } else { user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() }); - return { ...response, ...issueTokens(state, user, PROVIDER_PASSWORD) }; + return { ...response, ...issueTokens(state, user, PROVIDER_PASSWORD, { extraClaims }) }; } } type SignInWithIdpResponse = Schemas["GoogleCloudIdentitytoolkitV1SignInWithIdpResponse"]; -function signInWithIdp( +async function signInWithIdp( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithIdpRequest"] -): SignInWithIdpResponse { + reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithIdpRequest"], +): Promise { assert(!state.disableAuth, "PROJECT_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); if (reqBody.returnRefreshToken) { throw new NotImplementedError("returnRefreshToken is not implemented yet."); @@ -1431,7 +1506,7 @@ function signInWithIdp( const providerId = normalizedUri.searchParams.get("providerId")?.toLowerCase(); assert( providerId, - `INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential: ${normalizedUri.toString()}` + `INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential: ${normalizedUri.toString()}`, ); const oauthIdToken = normalizedUri.searchParams.get("id_token") || undefined; const oauthAccessToken = normalizedUri.searchParams.get("access_token") || undefined; @@ -1441,21 +1516,21 @@ function signInWithIdp( // Try to give the most helpful error message, depending on input. if (oauthIdToken) { throw new BadRequestError( - `INVALID_IDP_RESPONSE : Unable to parse id_token: ${oauthIdToken} ((Auth Emulator only accepts strict JSON or JWTs as fake id_tokens.))` + `INVALID_IDP_RESPONSE : Unable to parse id_token: ${oauthIdToken} ((Auth Emulator only accepts strict JSON or JWTs as fake id_tokens.))`, ); } else if (oauthAccessToken) { if (providerId === "google.com" || providerId === "apple.com") { throw new NotImplementedError( - `The Auth Emulator only support sign-in with ${providerId} using id_token, not access_token. Please update your code to use id_token.` + `The Auth Emulator only support sign-in with ${providerId} using id_token, not access_token. Please update your code to use id_token.`, ); } else { throw new NotImplementedError( - `The Auth Emulator does not support ${providerId} sign-in with credentials.` + `The Auth Emulator does not support ${providerId} sign-in with credentials.`, ); } } else { throw new NotImplementedError( - "The Auth Emulator only supports sign-in with credentials (id_token required)." + "The Auth Emulator only supports sign-in with credentials (id_token required).", ); } } @@ -1472,11 +1547,11 @@ function signInWithIdp( assert(samlResponse.assertion, "INVALID_IDP_RESPONSE ((Missing assertion in SAMLResponse.))"); assert( samlResponse.assertion.subject, - "INVALID_IDP_RESPONSE ((Missing assertion.subject in SAMLResponse.))" + "INVALID_IDP_RESPONSE ((Missing assertion.subject in SAMLResponse.))", ); assert( samlResponse.assertion.subject.nameId, - "INVALID_IDP_RESPONSE ((Missing assertion.subject.nameId in SAMLResponse.))" + "INVALID_IDP_RESPONSE ((Missing assertion.subject.nameId in SAMLResponse.))", ); } @@ -1504,15 +1579,15 @@ function signInWithIdp( response, rawId, userMatchingProvider, - userMatchingEmail + userMatchingEmail, )); } else { ({ accountUpdates, response } = handleIdpSigninEmailNotRequired( response, - userMatchingProvider + userMatchingProvider, )); } - } catch (err) { + } catch (err: any) { if (reqBody.returnIdpCredential && err instanceof BadRequestError) { response.errorMessage = err.message; return response; @@ -1538,27 +1613,89 @@ function signInWithIdp( }; let user: UserInfo; + let extraClaims; + const oauthTokens = { + oauthIdToken: response.oauthIdToken, + oauthAccessToken: response.oauthAccessToken, + + // The below are not set by our fake IdP fetch currently + oauthRefreshToken: response.oauthRefreshToken, + oauthTokenSecret: response.oauthTokenSecret, + oauthExpiresIn: coercePrimitiveToString(response.oauthExpireIn), + }; if (response.isNewUser) { - user = state.createUser({ + const timestamp = new Date(); + let updates: Partial = { ...accountUpdates.fields, - lastLoginAt: Date.now().toString(), + createdAt: timestamp.getTime().toString(), + lastLoginAt: timestamp.getTime().toString(), providerUserInfo: [providerUserInfo], tenantId: state instanceof TenantProjectState ? state.tenantId : undefined, - }); + }; + const localId = state.generateLocalId(); + const userBeforeCreate = { localId, ...updates }; + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_CREATE, + userBeforeCreate, + { + signInMethod: response.providerId, + rawUserInfo: response.rawUserInfo, + signInAttributes: JSON.stringify(signInAttributes), + }, + oauthTokens, + ); + + updates = { ...updates, ...blockingResponse.updates }; + user = state.createUserWithLocalId(localId, updates)!; response.localId = user.localId; + + if (!user.disabled && !isMfaEnabled(state, user)) { + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + user, + { + signInMethod: response.providerId, + rawUserInfo: response.rawUserInfo, + signInAttributes: JSON.stringify(signInAttributes), + }, + oauthTokens, + ); + updates = blockingResponse.updates; + extraClaims = blockingResponse.extraClaims; + user = state.updateUserByLocalId(user.localId, updates); + } } else { if (!response.localId) { - throw new Error("Internal assertion error: localId not set for exising user."); + throw new Error("Internal assertion error: localId not set for existing user."); } - user = state.updateUserByLocalId( - response.localId, - { - ...accountUpdates.fields, - }, - { - upsertProviders: [providerUserInfo], - } - ); + + const maybeUser = state.getUserByLocalId(response.localId); + assert(maybeUser, "USER_NOT_FOUND"); + user = maybeUser; + + let updates = { ...accountUpdates.fields }; + + if (!user.disabled && !isMfaEnabled(state, user)) { + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + { ...user, ...updates }, + { + signInMethod: response.providerId, + rawUserInfo: response.rawUserInfo, + signInAttributes: JSON.stringify(signInAttributes), + }, + oauthTokens, + ); + extraClaims = blockingResponse.extraClaims; + updates = { ...updates, ...blockingResponse.updates }; + } + + user = state.updateUserByLocalId(response.localId, updates, { + upsertProviders: [providerUserInfo], + }); } if (user.email === response.email) { @@ -1569,41 +1706,55 @@ function signInWithIdp( response.tenantId = state.tenantId; } - if ( - (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && - user.mfaInfo?.length - ) { + if (isMfaEnabled(state, user)) { return { ...response, ...mfaPending(state, user, providerId) }; } else { user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() }); - return { ...response, ...issueTokens(state, user, providerId, { signInAttributes }) }; + // User may have been disabled after either blocking function, but + // only throw after writing user to store + assert(!user?.disabled, "USER_DISABLED"); + return { + ...response, + ...issueTokens(state, user, providerId, { signInAttributes, extraClaims }), + }; } } -function signInWithPassword( +async function signInWithPassword( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithPasswordRequest"] -): Schemas["GoogleCloudIdentitytoolkitV1SignInWithPasswordResponse"] { + reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithPasswordRequest"], +): Promise { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); - assert(reqBody.email, "MISSING_EMAIL"); + assert(reqBody.email !== undefined, "MISSING_EMAIL"); + assert(isValidEmailAddress(reqBody.email), "INVALID_EMAIL"); assert(reqBody.password, "MISSING_PASSWORD"); if (reqBody.captchaResponse || reqBody.captchaChallenge) { throw new NotImplementedError("captcha unimplemented"); } if (reqBody.idToken || reqBody.pendingIdToken) { throw new NotImplementedError( - "idToken / pendingIdToken is no longer in use and unsupported by the Auth Emulator." + "idToken / pendingIdToken is no longer in use and unsupported by the Auth Emulator.", ); } const email = canonicalizeEmailAddress(reqBody.email); let user = state.getUserByEmail(email); - assert(user, "EMAIL_NOT_FOUND"); - assert(!user.disabled, "USER_DISABLED"); - assert(user.passwordHash && user.salt, "INVALID_PASSWORD"); - assert(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_PASSWORD"); + + if (state.enableImprovedEmailPrivacy) { + assert(user, "INVALID_LOGIN_CREDENTIALS"); + assert(!user.disabled, "USER_DISABLED"); + assert(user.passwordHash && user.salt, "INVALID_LOGIN_CREDENTIALS"); + assert( + user.passwordHash === hashPassword(reqBody.password, user.salt), + "INVALID_LOGIN_CREDENTIALS", + ); + } else { + assert(user, "EMAIL_NOT_FOUND"); + assert(!user.disabled, "USER_DISABLED"); + assert(user.passwordHash && user.salt, "INVALID_PASSWORD"); + assert(user.passwordHash === hashPassword(reqBody.password, user.salt), "INVALID_PASSWORD"); + } const response = { kind: "identitytoolkit#VerifyPasswordResponse", @@ -1612,24 +1763,32 @@ function signInWithPassword( email, }; - if ( - (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && - user.mfaInfo?.length - ) { + if (isMfaEnabled(state, user)) { return { ...response, ...mfaPending(state, user, PROVIDER_PASSWORD) }; } else { - user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() }); - return { ...response, ...issueTokens(state, user, PROVIDER_PASSWORD) }; + const { updates, extraClaims } = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + user, + { signInMethod: "password" }, + ); + user = state.updateUserByLocalId(user.localId, { + ...updates, + lastLoginAt: Date.now().toString(), + }); + // User may have been disabled after blocking function, but only throw after + // writing user to store + assert(!user.disabled, "USER_DISABLED"); + return { ...response, ...issueTokens(state, user, PROVIDER_PASSWORD, { extraClaims }) }; } } -function signInWithPhoneNumber( +async function signInWithPhoneNumber( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithPhoneNumberRequest"] -): Schemas["GoogleCloudIdentitytoolkitV1SignInWithPhoneNumberResponse"] { + reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithPhoneNumberRequest"], +): Promise { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(state instanceof AgentProjectState, "UNSUPPORTED_TENANT_OPERATION"); - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); let phoneNumber: string; if (reqBody.temporaryProof) { assert(reqBody.phoneNumber, "MISSING_PHONE_NUMBER"); @@ -1643,42 +1802,83 @@ function signInWithPhoneNumber( phoneNumber = verifyPhoneNumber(state, reqBody.sessionInfo, reqBody.code); } - let user = state.getUserByPhoneNumber(phoneNumber); - let isNewUser = false; - const updates = { + const userFromPhoneNumber = state.getUserByPhoneNumber(phoneNumber); + const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined; + if (userFromPhoneNumber && userFromIdToken) { + if (userFromPhoneNumber.localId !== userFromIdToken.localId) { + assert(!reqBody.temporaryProof, "PHONE_NUMBER_EXISTS"); + // By now, the verification has succeeded, but we cannot proceed since + // the phone number is linked to a different account. If a sessionInfo + // is consumed, a temporaryProof should be returned with 200. + return { + ...state.createTemporaryProof(phoneNumber), + }; + } + } + + let user = userFromIdToken || userFromPhoneNumber; + const isNewUser = !user; + + const timestamp = new Date(); + let updates: Partial = { phoneNumber, - lastLoginAt: Date.now().toString(), + lastLoginAt: timestamp.getTime().toString(), }; - const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined; + let extraClaims; if (!user) { - if (userFromIdToken) { - assert( - !userFromIdToken.mfaInfo?.length, - "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user." + updates.createdAt = timestamp.getTime().toString(); + const localId = state.generateLocalId(); + const userBeforeCreate = { localId, ...updates }; + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_CREATE, + userBeforeCreate, + { signInMethod: "phone" }, + ); + + updates = { ...updates, ...blockingResponse.updates }; + user = state.createUserWithLocalId(localId, updates)!; + + if (!user.disabled) { + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + user, + { signInMethod: "phone" }, ); - user = state.updateUserByLocalId(userFromIdToken.localId, updates); - } else { - isNewUser = true; - user = state.createUser(updates); + updates = blockingResponse.updates; + extraClaims = blockingResponse.extraClaims; + user = state.updateUserByLocalId(user.localId, updates); } } else { assert(!user.disabled, "USER_DISABLED"); - if (userFromIdToken && userFromIdToken.localId !== user.localId) { - if (!reqBody.temporaryProof) { - // By now, the verification has succeeded, but we cannot proceed since - // the phone number is linked to a different account. If a sessionInfo - // is consumed, a temporaryProof should be returned with 200. - return { - ...state.createTemporaryProof(phoneNumber), - }; - } - throw new BadRequestError("PHONE_NUMBER_EXISTS"); + assert( + !user.mfaInfo?.length, + "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user.", + ); + + if (!user.disabled) { + const blockingResponse = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + { ...user, ...updates }, + { signInMethod: "phone" }, + ); + updates = { ...updates, ...blockingResponse.updates }; + extraClaims = blockingResponse.extraClaims; } + user = state.updateUserByLocalId(user.localId, updates); } - const tokens = issueTokens(state, user, PROVIDER_PHONE); + // User may have been disabled after either blocking function, but + // only throw after writing user to store + assert(!user?.disabled, "USER_DISABLED"); + + const tokens = issueTokens(state, user, PROVIDER_PHONE, { + extraClaims, + }); return { isNewUser, @@ -1691,24 +1891,21 @@ function signInWithPhoneNumber( function grantToken( state: ProjectState, - reqBody: Schemas["GrantTokenRequest"] + reqBody: Schemas["GrantTokenRequest"], ): Schemas["GrantTokenResponse"] { // https://developers.google.com/identity/toolkit/reference/securetoken/rest/v1/token // reqBody.code is intentionally ignored. - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); assert(reqBody.grantType, "MISSING_GRANT_TYPE"); assert(reqBody.grantType === "refresh_token", "INVALID_GRANT_TYPE"); assert(reqBody.refreshToken, "MISSING_REFRESH_TOKEN"); const refreshTokenRecord = state.validateRefreshToken(reqBody.refreshToken); - assert(refreshTokenRecord, "INVALID_REFRESH_TOKEN"); assert(!refreshTokenRecord.user.disabled, "USER_DISABLED"); const tokens = issueTokens(state, refreshTokenRecord.user, refreshTokenRecord.provider, { extraClaims: refreshTokenRecord.extraClaims, secondFactor: refreshTokenRecord.secondFactor, }); return { - /* eslint-disable camelcase */ id_token: tokens.idToken, access_token: tokens.idToken, expires_in: tokens.expiresIn, @@ -1719,7 +1916,6 @@ function grantToken( // According to API docs (and production behavior), this should be the // automatically generated number, not the customizable alphanumeric ID. project_id: state.projectNumber, - /* eslint-enable camelcase */ }; } @@ -1733,37 +1929,29 @@ function getEmulatorProjectConfig(state: ProjectState): Schemas["EmulatorV1Proje signIn: { allowDuplicateEmails: !state.oneAccountPerEmail, }, - usageMode: state.usageMode, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: state.enableImprovedEmailPrivacy, + }, }; } function updateEmulatorProjectConfig( state: ProjectState, - reqBody: Schemas["EmulatorV1ProjectsConfig"] + reqBody: Schemas["EmulatorV1ProjectsConfig"], + ctx: ExegesisContext, ): Schemas["EmulatorV1ProjectsConfig"] { - const allowDuplicateEmails = reqBody.signIn?.allowDuplicateEmails; - if (allowDuplicateEmails != null) { - assert( - state instanceof AgentProjectState, - "((Only top level projects can set oneAccountPerEmail.))" - ); - state.oneAccountPerEmail = !allowDuplicateEmails; - } - const usageMode = reqBody.usageMode; - if (usageMode != null) { - assert(state instanceof AgentProjectState, "((Only top level projects can set usageMode.))"); - switch (usageMode) { - case "PASSTHROUGH": - assert(state.getUserCount() === 0, "Users are present, unable to set passthrough mode"); - state.usageMode = UsageMode.PASSTHROUGH; - break; - case "DEFAULT": - state.usageMode = UsageMode.DEFAULT; - break; - default: - throw new BadRequestError("Invalid usage mode provided"); - } + // New developers should not use updateEmulatorProjectConfig to update the + // allowDuplicateEmails setting and should instead use updateConfig to do so. + const updateMask = []; + if (reqBody.signIn?.allowDuplicateEmails != null) { + updateMask.push("signIn.allowDuplicateEmails"); + } + if (reqBody.emailPrivacyConfig?.enableImprovedEmailPrivacy != null) { + updateMask.push("emailPrivacyConfig.enableImprovedEmailPrivacy"); } + ctx.params.query.updateMask = updateMask.join(); + + updateConfig(state, reqBody, ctx); return getEmulatorProjectConfig(state); } @@ -1774,7 +1962,7 @@ function listOobCodesInProject(state: ProjectState): Schemas["EmulatorV1Projects } function listVerificationCodesInProject( - state: ProjectState + state: ProjectState, ): Schemas["EmulatorV1ProjectsVerificationCodes"] { return { verificationCodes: [...state.listVerificationCodes()], @@ -1783,24 +1971,24 @@ function listVerificationCodesInProject( function mfaEnrollmentStart( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV2StartMfaEnrollmentRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV2StartMfaEnrollmentRequest"], ): Schemas["GoogleCloudIdentitytoolkitV2StartMfaEnrollmentResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); assert( (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && state.mfaConfig.enabledProviders?.includes("PHONE_SMS"), - "OPERATION_NOT_ALLOWED : SMS based MFA not enabled." + "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.", ); assert(reqBody.idToken, "MISSING_ID_TOKEN"); const { user, signInProvider } = parseIdToken(state, reqBody.idToken); assert( !MFA_INELIGIBLE_PROVIDER.has(signInProvider), - "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor." + "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor.", ); assert( user.emailVerified, - "UNVERIFIED_EMAIL : Need to verify email first before enrolling second factors." + "UNVERIFIED_EMAIL : Need to verify email first before enrolling second factors.", ); assert(reqBody.phoneEnrollmentInfo, "INVALID_ARGUMENT : ((Missing phoneEnrollmentInfo.))"); @@ -1815,7 +2003,7 @@ function mfaEnrollmentStart( assert(phoneNumber && isValidPhoneNumber(phoneNumber), "INVALID_PHONE_NUMBER : Invalid format."); assert( !user.mfaInfo?.some((enrollment) => enrollment.unobfuscatedPhoneInfo === phoneNumber), - "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account." + "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account.", ); const { sessionInfo, code } = state.createVerificationCode(phoneNumber); @@ -1824,7 +2012,7 @@ function mfaEnrollmentStart( // a real text message out to the phone number. EmulatorLogger.forEmulator(Emulators.AUTH).log( "BULLET", - `To enroll MFA with ${phoneNumber}, use the code ${code}.` + `To enroll MFA with ${phoneNumber}, use the code ${code}.`, ); return { @@ -1836,19 +2024,19 @@ function mfaEnrollmentStart( function mfaEnrollmentFinalize( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentRequest"], ): Schemas["GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); assert( (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && state.mfaConfig.enabledProviders?.includes("PHONE_SMS"), - "OPERATION_NOT_ALLOWED : SMS based MFA not enabled." + "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.", ); assert(reqBody.idToken, "MISSING_ID_TOKEN"); let { user, signInProvider } = parseIdToken(state, reqBody.idToken); assert( !MFA_INELIGIBLE_PROVIDER.has(signInProvider), - "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor." + "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor.", ); assert(reqBody.phoneVerificationInfo, "INVALID_ARGUMENT : ((Missing phoneVerificationInfo.))"); @@ -1863,7 +2051,7 @@ function mfaEnrollmentFinalize( const phoneNumber = verifyPhoneNumber(state, sessionInfo, code); assert( !user.mfaInfo?.some((enrollment) => enrollment.unobfuscatedPhoneInfo === phoneNumber), - "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account." + "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account.", ); const existingFactors = user.mfaInfo || []; @@ -1898,7 +2086,7 @@ function mfaEnrollmentFinalize( function mfaEnrollmentWithdraw( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV2WithdrawMfaRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV2WithdrawMfaRequest"], ): Schemas["GoogleCloudIdentitytoolkitV2WithdrawMfaResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); assert(reqBody.idToken, "MISSING_ID_TOKEN"); @@ -1907,7 +2095,7 @@ function mfaEnrollmentWithdraw( assert(user.mfaInfo, "MFA_ENROLLMENT_NOT_FOUND"); const updatedList = user.mfaInfo.filter( - (enrollment) => enrollment.mfaEnrollmentId !== reqBody.mfaEnrollmentId + (enrollment) => enrollment.mfaEnrollmentId !== reqBody.mfaEnrollmentId, ); assert(updatedList.length < user.mfaInfo.length, "MFA_ENROLLMENT_NOT_FOUND"); @@ -1920,21 +2108,21 @@ function mfaEnrollmentWithdraw( function mfaSignInStart( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV2StartMfaSignInRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV2StartMfaSignInRequest"], ): Schemas["GoogleCloudIdentitytoolkitV2StartMfaSignInResponse"] { assert(!state.disableAuth, "PROJECT_DISABLED"); assert( (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && state.mfaConfig.enabledProviders?.includes("PHONE_SMS"), - "OPERATION_NOT_ALLOWED : SMS based MFA not enabled." + "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.", ); assert( reqBody.mfaPendingCredential, - "MISSING_MFA_PENDING_CREDENTIAL : Request does not have MFA pending credential." + "MISSING_MFA_PENDING_CREDENTIAL : Request does not have MFA pending credential.", ); assert( reqBody.mfaEnrollmentId, - "MISSING_MFA_ENROLLMENT_ID : No second factor identifier is provided." + "MISSING_MFA_ENROLLMENT_ID : No second factor identifier is provided.", ); // In production, reqBody.phoneSignInInfo must be set to indicate phone-based // MFA. However, we don't enforce this because none of its fields are required @@ -1942,7 +2130,7 @@ function mfaSignInStart( const { user } = parsePendingCredential(state, reqBody.mfaPendingCredential); const enrollment = user.mfaInfo?.find( - (factor) => factor.mfaEnrollmentId === reqBody.mfaEnrollmentId + (factor) => factor.mfaEnrollmentId === reqBody.mfaEnrollmentId, ); assert(enrollment, "MFA_ENROLLMENT_NOT_FOUND"); const phoneNumber = enrollment.unobfuscatedPhoneInfo; @@ -1954,7 +2142,7 @@ function mfaSignInStart( // a real text message out to the phone number. EmulatorLogger.forEmulator(Emulators.AUTH).log( "BULLET", - `To sign in with MFA using ${phoneNumber}, use the code ${code}.` + `To sign in with MFA using ${phoneNumber}, use the code ${code}.`, ); return { @@ -1964,15 +2152,15 @@ function mfaSignInStart( }; } -function mfaSignInFinalize( +async function mfaSignInFinalize( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV2FinalizeMfaSignInRequest"] -): Schemas["GoogleCloudIdentitytoolkitV2FinalizeMfaSignInResponse"] { + reqBody: Schemas["GoogleCloudIdentitytoolkitV2FinalizeMfaSignInRequest"], +): Promise { assert(!state.disableAuth, "PROJECT_DISABLED"); assert( (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && state.mfaConfig.enabledProviders?.includes("PHONE_SMS"), - "OPERATION_NOT_ALLOWED : SMS based MFA not enabled." + "OPERATION_NOT_ALLOWED : SMS based MFA not enabled.", ); // Inconsistent with mfaSignInStart (where MISSING_MFA_PENDING_CREDENTIAL is // returned), but matches production behavior. @@ -1990,15 +2178,27 @@ function mfaSignInFinalize( let { user, signInProvider } = parsePendingCredential(state, reqBody.mfaPendingCredential); const enrollment = user.mfaInfo?.find( - (enrollment) => enrollment.unobfuscatedPhoneInfo == phoneNumber + (enrollment) => enrollment.unobfuscatedPhoneInfo === phoneNumber, ); - assert(enrollment && enrollment.mfaEnrollmentId, "MFA_ENROLLMENT_NOT_FOUND"); - user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() }); + const { updates, extraClaims } = await fetchBlockingFunction( + state, + BlockingFunctionEvents.BEFORE_SIGN_IN, + user, + { signInMethod: signInProvider, signInSecondFactor: "phone" }, + ); + user = state.updateUserByLocalId(user.localId, { + ...updates, + lastLoginAt: Date.now().toString(), + }); + assert(enrollment && enrollment.mfaEnrollmentId, "MFA_ENROLLMENT_NOT_FOUND"); + // User may have been disabled after blocking function, but only throw after + // writing user to store assert(!user.disabled, "USER_DISABLED"); const { idToken, refreshToken } = issueTokens(state, user, signInProvider, { + extraClaims, secondFactor: { identifier: enrollment.mfaEnrollmentId, provider: PROVIDER_PHONE }, }); return { @@ -2007,10 +2207,43 @@ function mfaSignInFinalize( }; } +function getConfig(state: ProjectState): Schemas["GoogleCloudIdentitytoolkitAdminV2Config"] { + // Shouldn't error on this but need assertion for type checking + assert( + state instanceof AgentProjectState, + "((Can only get top-level configurations on agent projects.))", + ); + return state.config; +} + +function updateConfig( + state: ProjectState, + reqBody: Schemas["GoogleCloudIdentitytoolkitAdminV2Config"], + ctx: ExegesisContext, +): Schemas["GoogleCloudIdentitytoolkitAdminV2Config"] { + assert( + state instanceof AgentProjectState, + "((Can only update top-level configurations on agent projects.))", + ); + for (const event in reqBody.blockingFunctions?.triggers) { + if (Object.prototype.hasOwnProperty.call(reqBody.blockingFunctions!.triggers, event)) { + assert( + Object.values(BlockingFunctionEvents).includes(event as BlockingFunctionEvents), + "INVALID_BLOCKING_FUNCTION : ((Event type is invalid.))", + ); + assert( + parseAbsoluteUri(reqBody.blockingFunctions!.triggers[event].functionUri!), + "INVALID_BLOCKING_FUNCTION : ((Expected an absolute URI with valid scheme and host.))", + ); + } + } + return state.updateConfig(reqBody, ctx.params.query.updateMask); +} + export type AuthOperation = ( state: ProjectState, reqBody: object, - ctx: ExegesisContext + ctx: ExegesisContext, ) => Promise | object; export type AuthOps = { @@ -2056,12 +2289,10 @@ function issueTokens( extraClaims?: Record; secondFactor?: SecondFactorRecord; signInAttributes?: unknown; - } = {} + } = {}, ): { idToken: string; refreshToken?: string; expiresIn: string } { user = state.updateUserByLocalId(user.localId, { lastRefreshAt: new Date().toISOString() }); - const usageMode = state.usageMode === UsageMode.PASSTHROUGH ? "passthrough" : undefined; - const tenantId = state instanceof TenantProjectState ? state.tenantId : undefined; const expiresInSeconds = 60 * 60; @@ -2071,17 +2302,13 @@ function issueTokens( expiresInSeconds, extraClaims, secondFactor, - usageMode, tenantId, signInAttributes, }); - const refreshToken = - state.usageMode === UsageMode.DEFAULT - ? state.createRefreshTokenFor(user, signInProvider, { - extraClaims, - secondFactor, - }) - : undefined; + const refreshToken = state.createRefreshTokenFor(user, signInProvider, { + extraClaims, + secondFactor, + }); return { idToken, refreshToken, @@ -2091,14 +2318,13 @@ function issueTokens( function parseIdToken( state: ProjectState, - idToken: string + idToken: string, ): { user: UserInfo; payload: FirebaseJwtPayload; signInProvider: string; } { - assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION"); - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -2111,13 +2337,13 @@ function parseIdToken( // request will most likely fail below with USER_NOT_FOUND. EmulatorLogger.forEmulator(Emulators.AUTH).log( "WARN", - "Received a signed JWT. Auth Emulator does not validate JWTs and IS NOT SECURE" + "Received a signed JWT. Auth Emulator does not validate JWTs and IS NOT SECURE", ); } if (decoded.payload.firebase.tenant) { assert( state instanceof TenantProjectState, - "((Parsed token that belongs to tenant in a non-tenant project.))" + "((Parsed token that belongs to tenant in a non-tenant project.))", ); assert(decoded.payload.firebase.tenant === state.tenantId, "TENANT_ID_MISMATCH"); } @@ -2140,7 +2366,6 @@ function generateJwt( expiresInSeconds, extraClaims = {}, secondFactor, - usageMode, tenantId, signInAttributes, }: { @@ -2149,10 +2374,9 @@ function generateJwt( expiresInSeconds: number; extraClaims?: Record; secondFactor?: SecondFactorRecord; - usageMode?: string; tenantId?: string; signInAttributes?: unknown; - } + }, ): string { const identities: Record = {}; if (user.email) { @@ -2173,7 +2397,6 @@ function generateJwt( } const customAttributes = JSON.parse(user.customAttributes || "{}") as Record; - /* eslint-disable camelcase */ const customPayloadFields: Partial = { // Non-reserved fields (set before custom attributes): name: user.displayName, @@ -2196,25 +2419,31 @@ function generateJwt( sign_in_provider: signInProvider, second_factor_identifier: secondFactor?.identifier, sign_in_second_factor: secondFactor?.provider, - usage_mode: usageMode, tenant: tenantId, sign_in_attributes: signInAttributes, }, }; - /* eslint-enable camelcase */ - const jwtStr = signJwt(customPayloadFields, "", { - // Generate a unsigned (insecure) JWT. This is accepted by many other - // emulators (e.g. Cloud Firestore Emulator) but will not work in - // production of course. This removes the need to sign / verify tokens. - algorithm: "none", - expiresIn: expiresInSeconds, + const jwtStr = signJwt( + customPayloadFields, + // secretOrPrivateKey is required for jsonwebtoken v9, see + // https://github.com/auth0/node-jsonwebtoken/wiki/Migration-Notes:-v8-to-v9 + // Tokens generated by the auth emulator are intentionally insecure and are + // not meant to be used in production. Thus, a fake secret is used here. + "fake-secret", + { + // Generate a unsigned (insecure) JWT. This is accepted by many other + // emulators (e.g. Cloud Firestore Emulator) but will not work in + // production of course. This removes the need to sign / verify tokens. + algorithm: "none", + expiresIn: expiresInSeconds, - subject: user.localId, - // TODO: Should this point to an emulator URL? - issuer: `https://securetoken.google.com/${projectId}`, - audience: projectId, - }); + subject: user.localId, + // TODO: Should this point to an emulator URL? + issuer: `https://securetoken.google.com/${projectId}`, + audience: projectId, + }, + ); return jwtStr; } @@ -2230,7 +2459,7 @@ function getAuthTime(user: UserInfo): Date { const authTime = new Date(user.lastRefreshAt); // Parse from ISO date string. if (isNaN(authTime.getTime())) { throw new Error( - `Internal assertion error: invalid user.lastRefreshAt = ${user.lastRefreshAt}` + `Internal assertion error: invalid user.lastRefreshAt = ${user.lastRefreshAt}`, ); } return authTime; @@ -2303,14 +2532,14 @@ function newRandomId(length: number, existingIds?: Set): string { } throw new InternalError( "INTERNAL_ERROR : Failed to generate a random ID after 10 attempts", - "INTERNAL" + "INTERNAL", ); } function getMfaEnrollmentsFromRequest( state: ProjectState, request: MfaEnrollments, - options?: { generateEnrollmentIds: boolean } + options?: { generateEnrollmentIds: boolean }, ): MfaEnrollments { const enrollments: MfaEnrollments = []; const phoneNumbers: Set = new Set(); @@ -2318,7 +2547,7 @@ function getMfaEnrollmentsFromRequest( for (const enrollment of request) { assert( enrollment.phoneInfo && isValidPhoneNumber(enrollment.phoneInfo), - "INVALID_MFA_PHONE_NUMBER : Invalid format." + "INVALID_MFA_PHONE_NUMBER : Invalid format.", ); if (!phoneNumbers.has(enrollment.phoneInfo)) { const mfaEnrollmentId = options?.generateEnrollmentIds @@ -2373,11 +2602,11 @@ function parseClaims(idTokenOrJsonClaims: string | undefined): IdpJwtPayload | u claims = JSON.parse(idTokenOrJsonClaims); } catch { throw new BadRequestError( - `INVALID_IDP_RESPONSE : Unable to parse id_token: ${idTokenOrJsonClaims} ((Auth Emulator failed to parse fake id_token as strict JSON.))` + `INVALID_IDP_RESPONSE : Unable to parse id_token: ${idTokenOrJsonClaims} ((Auth Emulator failed to parse fake id_token as strict JSON.))`, ); } } else { - const decoded = decodeJwt(idTokenOrJsonClaims, { json: true }); + const decoded = decodeJwt(idTokenOrJsonClaims, { json: true }) as any; if (!decoded) { return undefined; } @@ -2386,11 +2615,11 @@ function parseClaims(idTokenOrJsonClaims: string | undefined): IdpJwtPayload | u assert( claims.sub, - 'INVALID_IDP_RESPONSE : Invalid Idp Response: id_token missing required fields. ((Missing "sub" field. This field is required and must be a unique identifier.))' + 'INVALID_IDP_RESPONSE : Invalid Idp Response: id_token missing required fields. ((Missing "sub" field. This field is required and must be a unique identifier.))', ); assert( typeof claims.sub === "string", - 'INVALID_IDP_RESPONSE : ((The "sub" field must be a string.))' + 'INVALID_IDP_RESPONSE : ((The "sub" field must be a string.))', ); return claims; } @@ -2398,7 +2627,7 @@ function parseClaims(idTokenOrJsonClaims: string | undefined): IdpJwtPayload | u function fakeFetchUserInfoFromIdp( providerId: string, claims: IdpJwtPayload, - samlResponse?: SamlResponse + samlResponse?: SamlResponse, ): { response: SignInWithIdpResponse; rawId: string; @@ -2425,18 +2654,17 @@ function fakeFetchUserInfoFromIdp( }; let federatedId = rawId; - /* eslint-disable camelcase */ switch (providerId) { case "google.com": { federatedId = `https://accounts.google.com/${rawId}`; - let granted_scopes = "openid https://www.googleapis.com/auth/userinfo.profile"; + let grantedScopes = "openid https://www.googleapis.com/auth/userinfo.profile"; if (email) { - granted_scopes += " https://www.googleapis.com/auth/userinfo.email"; + grantedScopes += " https://www.googleapis.com/auth/userinfo.email"; } response.firstName = claims.given_name; response.lastName = claims.family_name; response.rawUserInfo = JSON.stringify({ - granted_scopes, + granted_scopes: grantedScopes, id: rawId, name: displayName, given_name: claims.given_name, @@ -2473,7 +2701,7 @@ interface AccountUpdates { function handleLinkIdp( state: ProjectState, response: SignInWithIdpResponse, - userFromIdToken: UserInfo + userFromIdToken: UserInfo, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -2482,7 +2710,7 @@ function handleLinkIdp( const userMatchingEmail = state.getUserByEmail(response.email); assert( !userMatchingEmail || userMatchingEmail.localId === userFromIdToken.localId, - "EMAIL_EXISTS" + "EMAIL_EXISTS", ); } response.localId = userFromIdToken.localId; @@ -2503,7 +2731,7 @@ function handleLinkIdp( function handleIdpSigninEmailNotRequired( response: SignInWithIdpResponse, - userMatchingProvider: UserInfo | undefined + userMatchingProvider: UserInfo | undefined, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -2523,7 +2751,7 @@ function handleIdpSigninEmailRequired( response: SignInWithIdpResponse, rawId: string, userMatchingProvider: UserInfo | undefined, - userMatchingEmail: UserInfo | undefined + userMatchingEmail: UserInfo | undefined, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -2538,7 +2766,7 @@ function handleIdpSigninEmailRequired( if (response.emailVerified) { if ( userMatchingEmail.providerUserInfo?.some( - (info) => info.providerId === response.providerId && info.rawId !== rawId + (info) => info.providerId === response.providerId && info.rawId !== rawId, ) ) { // b/6793858: An account exists with the same email but different rawId, @@ -2558,7 +2786,7 @@ function handleIdpSigninEmailRequired( accountUpdates.fields.phoneNumber = undefined; accountUpdates.fields.validSince = toUnixTimestamp(new Date()).toString(); accountUpdates.deleteProviders = userMatchingEmail.providerUserInfo?.map( - (info) => info.providerId + (info) => info.providerId, ); } @@ -2586,7 +2814,7 @@ function handleIdpSigninEmailRequired( function handleIdpSignUp( response: SignInWithIdpResponse, - options: { emailRequired: boolean } + options: { emailRequired: boolean }, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -2630,7 +2858,7 @@ interface MfaPendingCredential { function mfaPending( state: ProjectState, user: UserInfo, - signInProvider: string + signInProvider: string, ): { mfaPendingCredential: string; mfaInfo: MfaEnrollment[] } { if (!user.mfaInfo) { throw new Error("Internal assertion error: mfaPending called on user without MFA."); @@ -2649,7 +2877,7 @@ function mfaPending( // data in the Auth Emulator but just trust developers not to modify it. const mfaPendingCredential = Buffer.from( JSON.stringify(pendingCredentialPayload), - "utf8" + "utf8", ).toString("base64"); return { mfaPendingCredential, mfaInfo: user.mfaInfo.map(redactMfaInfo) }; @@ -2684,7 +2912,7 @@ function obfuscatePhoneNumber(phoneNumber: string): string { function parsePendingCredential( state: ProjectState, - pendingCredential: string + pendingCredential: string, ): { user: UserInfo; signInProvider: string; @@ -2698,16 +2926,16 @@ function parsePendingCredential( } assert( pendingCredentialPayload._AuthEmulatorMfaPendingCredential, - "((Invalid phoneVerificationInfo.mfaPendingCredential.))" + "((Invalid phoneVerificationInfo.mfaPendingCredential.))", ); assert( pendingCredentialPayload.projectId === state.projectId, - "INVALID_PROJECT_ID : Project ID does not match MFA pending credential." + "INVALID_PROJECT_ID : Project ID does not match MFA pending credential.", ); if (state instanceof TenantProjectState) { assert( pendingCredentialPayload.tenantId === state.tenantId, - "INVALID_PROJECT_ID : Project ID does not match MFA pending credential." + "INVALID_PROJECT_ID : Project ID does not match MFA pending credential.", ); } @@ -2720,10 +2948,10 @@ function parsePendingCredential( function createTenant( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] + reqBody: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], ): Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] { if (!(state instanceof AgentProjectState)) { - throw new InternalError("INTERNAL_ERROR: Can only create tenant in agent project", "INTERNAL"); + throw new InternalError("INTERNAL_ERROR : Can only create tenant in agent project", "INTERNAL"); } const mfaConfig = reqBody.mfaConfig ?? {}; @@ -2751,7 +2979,7 @@ function createTenant( function listTenants( state: ProjectState, reqBody: unknown, - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitAdminV2ListTenantsResponse"] { assert(state instanceof AgentProjectState, "((Can only list tenants in agent project.))"); const pageSize = Math.min(Math.floor(ctx.params.query.pageSize) || 20, 1000); @@ -2771,21 +2999,13 @@ function listTenants( }; } -function deleteTenant( - state: ProjectState, - reqBody: unknown, - ctx: ExegesisContext -): Schemas["GoogleProtobufEmpty"] { +function deleteTenant(state: ProjectState): Schemas["GoogleProtobufEmpty"] { assert(state instanceof TenantProjectState, "((Can only delete tenant on tenant projects.))"); state.delete(); return {}; } -function getTenant( - state: ProjectState, - reqBody: unknown, - ctx: ExegesisContext -): Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] { +function getTenant(state: ProjectState): Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] { assert(state instanceof TenantProjectState, "((Can only get tenant on tenant projects.))"); return state.tenantConfig; } @@ -2793,12 +3013,295 @@ function getTenant( function updateTenant( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] { assert(state instanceof TenantProjectState, "((Can only update tenant on tenant projects.))"); return state.updateTenant(reqBody, ctx.params.query.updateMask); } +function isMfaEnabled(state: ProjectState, user: UserInfo) { + return ( + (state.mfaConfig.state === "ENABLED" || state.mfaConfig.state === "MANDATORY") && + user.mfaInfo?.length + ); +} + +// TODO: Timeout is 60s. Should we make the timeout an emulator configuration? +async function fetchBlockingFunction( + state: ProjectState, + event: BlockingFunctionEvents, + user: UserInfo, + options: { + signInMethod?: string; + signInSecondFactor?: string; + rawUserInfo?: string; + signInAttributes?: string; + } = {}, + oauthTokens: { + oauthIdToken?: string; + oauthAccessToken?: string; + oauthRefreshToken?: string; + oauthTokenSecret?: string; + oauthExpiresIn?: string; + } = {}, + timeoutMs: number = 60000, +): Promise<{ + updates: BlockingFunctionUpdates; + extraClaims?: Record; +}> { + const url = state.getBlockingFunctionUri(event); + + // No-op if blocking function is not present + if (!url) { + return { updates: {} }; + } + + const jwt = generateBlockingFunctionJwt(state, event, url, timeoutMs, user, options, oauthTokens); + const reqBody = { + data: { + jwt, + }, + }; + + const controller = new AbortController(); + const timeout = setTimeout(() => { + controller.abort(); + }, timeoutMs); + + let response: BlockingFunctionResponsePayload; + let ok: boolean; + let status: number; + let text: string; + try { + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(reqBody), + signal: controller.signal, + }); + ok = res.ok; + status = res.status; + text = await res.text(); + } catch (thrown: any) { + const err = thrown instanceof Error ? thrown : new Error(thrown); + const isAbortError = err.name.includes("AbortError"); + if (isAbortError) { + throw new InternalError( + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((Deadline exceeded making request to ${url}.))`, + err.message, + ); + } + // All other server errors + throw new InternalError( + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((Failed to make request to ${url}.))`, + err.message, + ); + } finally { + clearTimeout(timeout); + } + + assert( + ok, + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((HTTP request to ${url} returned HTTP error ${status}: ${text}))`, + ); + + try { + response = JSON.parse(text) as BlockingFunctionResponsePayload; + } catch (thrown: any) { + const err = thrown instanceof Error ? thrown : new Error(thrown); + throw new InternalError( + `BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response body is not valid JSON.))`, + err.message, + ); + } + + return processBlockingFunctionResponse(event, response); +} + +function processBlockingFunctionResponse( + event: BlockingFunctionEvents, + response: BlockingFunctionResponsePayload, +): { + updates: BlockingFunctionUpdates; + extraClaims?: Record; +} { + // Only return updates that are specified in the update mask + let extraClaims; + const updates: BlockingFunctionUpdates = {}; + if (response.userRecord) { + const userRecord = response.userRecord; + assert( + userRecord.updateMask, + "BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response UserRecord is missing updateMask.))", + ); + const mask = userRecord.updateMask; + const fields = mask.split(","); + + for (const field of fields) { + switch (field) { + case "displayName": + case "photoUrl": + updates[field] = coercePrimitiveToString(userRecord[field]); + break; + case "disabled": + case "emailVerified": + updates[field] = !!userRecord[field]; + break; + case "customClaims": + const customClaims = JSON.stringify(userRecord.customClaims!); + validateSerializedCustomClaims(customClaims); + updates.customAttributes = customClaims; + break; + // Session claims are only returned in beforeSignIn and will be ignored + // otherwise. For more info, see + // https://cloud.google.com/identity-platform/docs/blocking-functions#modifying_a_user + case "sessionClaims": + if (event !== BlockingFunctionEvents.BEFORE_SIGN_IN) { + break; + } + try { + extraClaims = userRecord.sessionClaims; + } catch { + throw new BadRequestError( + "BLOCKING_FUNCTION_ERROR_RESPONSE : ((Response has malformed session claims.))", + ); + } + break; + default: + break; + } + } + } + + return { updates, extraClaims }; +} + +function generateBlockingFunctionJwt( + state: ProjectState, + event: BlockingFunctionEvents, + url: string, + timeoutMs: number, + user: UserInfo, + options: { + signInMethod?: string; + signInSecondFactor?: string; + rawUserInfo?: string; + signInAttributes?: string; + }, + oauthTokens: { + oauthIdToken?: string; + oauthAccessToken?: string; + oauthRefreshToken?: string; + oauthTokenSecret?: string; + oauthExpiresIn?: string; + }, +): string { + const issuedAt = toUnixTimestamp(new Date()); + const jwt: BlockingFunctionsJwtPayload = { + iss: `https://securetoken.google.com/${state.projectId}`, + aud: url, + iat: issuedAt, + exp: issuedAt + timeoutMs / 100, + event_id: randomBase64UrlStr(16), + event_type: event, + user_agent: "NotYetSupportedInFirebaseAuthEmulator", // TODO: switch to express.js to get UserAgent + ip_address: "127.0.0.1", // TODO: switch to express.js to get IP address + locale: "en", + user_record: { + uid: user.localId, + email: user.email, + email_verified: user.emailVerified, + display_name: user.displayName, + photo_url: user.photoUrl, + disabled: user.disabled, + phone_number: user.phoneNumber, + custom_claims: JSON.parse(user.customAttributes || "{}") as Record, + }, + sub: user.localId, + sign_in_method: options.signInMethod, + sign_in_second_factor: options.signInSecondFactor, + sign_in_attributes: options.signInAttributes, + raw_user_info: options.rawUserInfo, + }; + + if (state instanceof TenantProjectState) { + jwt.tenant_id = state.tenantId; + jwt.user_record.tenant_id = state.tenantId; + } + + const providerData = []; + if (user.providerUserInfo) { + for (const providerUserInfo of user.providerUserInfo) { + const provider: Provider = { + provider_id: providerUserInfo.providerId, + display_name: providerUserInfo.displayName, + photo_url: providerUserInfo.photoUrl, + email: providerUserInfo.email, + uid: providerUserInfo.rawId, + phone_number: providerUserInfo.phoneNumber, + }; + providerData.push(provider); + } + } + jwt.user_record.provider_data = providerData; + + if (user.mfaInfo) { + const enrolledFactors = []; + for (const mfaEnrollment of user.mfaInfo) { + if (!mfaEnrollment.mfaEnrollmentId) { + continue; + } + const enrolledFactor: EnrolledFactor = { + uid: mfaEnrollment.mfaEnrollmentId, + display_name: mfaEnrollment.displayName, + enrollment_time: mfaEnrollment.enrolledAt, + phone_number: mfaEnrollment.phoneInfo, + factor_id: PROVIDER_PHONE, + }; + enrolledFactors.push(enrolledFactor); + } + jwt.user_record.multi_factor = { + enrolled_factors: enrolledFactors, + }; + } + + if (user.lastLoginAt || user.createdAt) { + jwt.user_record.metadata = { + last_sign_in_time: user.lastLoginAt, + creation_time: user.createdAt, + }; + } + + if (state.shouldForwardCredentialToBlockingFunction("accessToken")) { + jwt.oauth_access_token = oauthTokens.oauthAccessToken; + jwt.oauth_token_secret = oauthTokens.oauthTokenSecret; + jwt.oauth_expires_in = oauthTokens.oauthExpiresIn; + } + + if (state.shouldForwardCredentialToBlockingFunction("idToken")) { + jwt.oauth_id_token = oauthTokens.oauthIdToken; + } + + if (state.shouldForwardCredentialToBlockingFunction("refreshToken")) { + jwt.oauth_refresh_token = oauthTokens.oauthRefreshToken; + } + + const jwtStr = signJwt(jwt, "fake-secret", { + algorithm: "none", + }); + + return jwtStr; +} + +export function parseBlockingFunctionJwt(jwt: string): BlockingFunctionsJwtPayload { + const decoded = decodeJwt(jwt, { json: true }) as any as BlockingFunctionsJwtPayload; + assert(decoded, "((Invalid blocking function jwt.))"); + assert(decoded.iss, "((Invalid blocking function jwt, missing `iss` claim.))"); + assert(decoded.aud, "((Invalid blocking function jwt, missing `aud` claim.))"); + assert(decoded.user_record, "((Invalid blocking function jwt, missing `user_record` claim.))"); + return decoded; +} + export interface SamlAssertion { subject?: { nameId?: string; @@ -2810,7 +3313,6 @@ export interface SamlResponse { assertion?: SamlAssertion; } -/* eslint-disable camelcase */ export interface FirebaseJwtPayload { // Standard fields: iat: number; // issuedAt (in seconds since epoch) @@ -2838,7 +3340,6 @@ export interface FirebaseJwtPayload { sign_in_provider: string; sign_in_second_factor?: string; second_factor_identifier?: string; - usage_mode?: string; tenant?: string; sign_in_attributes?: unknown; }; @@ -2862,7 +3363,7 @@ export interface IdpJwtPayload { /** Unique identifier of user at IDP. Also known as "rawId" in Firebase Auth. */ sub: string; - // Issuer (IDP identifer / URL) and Audience (Developer app ID), ignored. + // Issuer (IDP identifier / URL) and Audience (Developer app ID), ignored. iss: string; // Ignored aud: string; // Ignored @@ -2940,4 +3441,93 @@ export interface IdpJwtPayload { locale?: string; hd?: string; } -/* eslint-enable camelcase */ + +export interface BlockingFunctionResponsePayload { + userRecord?: { + updateMask?: string; + displayName?: string; + photoUrl?: string; + disabled?: boolean; + emailVerified?: boolean; + customClaims?: Record; + sessionClaims?: Record; + }; +} + +export interface BlockingFunctionUpdates { + displayName?: string; + photoUrl?: string; + disabled?: boolean; + emailVerified?: boolean; + customAttributes?: string; +} + +/** + * Information corresponding to a sign in provider. + */ +export interface Provider { + provider_id?: string; + display_name?: string; + photo_url?: string; + email?: string; + uid?: string; + phone_number?: string; +} + +/** + * Enrolled factors for MFA. + */ +export interface EnrolledFactor { + uid: string; + display_name?: string; + enrollment_time?: string; + phone_number?: string; + factor_id: string; +} + +/** + * Typing for payload passed to blocking function requests. + */ +export interface BlockingFunctionsJwtPayload { + iss: string; // issuer (=`https://securetoken.google.com/{projectId}`) + aud: string; // audience (=`{functionUri}`) + iat: number; // issuedAt (in seconds since epoch) + exp: number; // expiresAt (in seconds since epoch) + event_id: string; // event identifier (=randomly generated base 64 string) + event_type: string; // one of BlockingFunctionEvents + user_agent: string; + ip_address: string; + locale: string; + user_record: { + uid?: string; + email?: string; + email_verified?: boolean; + display_name?: string; + photo_url?: string; + disabled?: boolean; + phone_number?: string; + provider_data?: Provider[]; + multi_factor?: { + enrolled_factors: EnrolledFactor[]; + }; + metadata?: { + last_sign_in_time?: string; + creation_time?: string; + }; + custom_claims?: Record; + tenant_id?: string; // should match top level tenant_id + }; + tenant_id?: string; // `tenantId` if present + sign_in_method?: string; + sign_in_second_factor?: string; + sign_in_attributes?: string; + raw_user_info?: string; + sub?: string; + + // Presence of these fields depends on blocking functions configuration + oauth_id_token?: string; + oauth_access_token?: string; + oauth_token_secret?: string; + oauth_refresh_token?: string; + oauth_expires_in?: string; +} diff --git a/src/emulator/auth/password.spec.ts b/src/emulator/auth/password.spec.ts new file mode 100644 index 00000000000..ef66b7a9583 --- /dev/null +++ b/src/emulator/auth/password.spec.ts @@ -0,0 +1,433 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; +import { FirebaseJwtPayload } from "./operations"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; +import { + BEFORE_SIGN_IN_PATH, + BEFORE_SIGN_IN_URL, + BLOCKING_FUNCTION_HOST, + DISPLAY_NAME, + expectStatusCode, + getAccountInfoByLocalId, + PHOTO_URL, + registerTenant, + registerUser, + TEST_MFA_INFO, + updateAccountByLocalId, + updateConfig, +} from "./testing/helpers"; + +describeAuthEmulator("accounts:signInWithPassword", ({ authApi, getClock }) => { + it("should issue tokens when email and password are valid", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { localId } = await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).equals(localId); + expect(res.body.email).equals(user.email); + expect(res.body).to.have.property("registered").equals(true); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.user_id).to.equal(localId); + expect(decoded!.payload).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); + }); + }); + + it("should update lastLoginAt on successful login", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { localId } = await registerUser(authApi(), user); + + const beforeLogin = await getAccountInfoByLocalId(authApi(), localId); + expect(beforeLogin.lastLoginAt).to.equal(Date.now().toString()); + + getClock().tick(4000); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }) + .then((res) => { + expectStatusCode(200, res); + }); + + const afterLogin = await getAccountInfoByLocalId(authApi(), localId); + expect(afterLogin.lastLoginAt).to.equal(Date.now().toString()); + }); + + it("should validate email address ignoring case", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { localId } = await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "AlIcE@exAMPle.COM", password: user.password }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).equals(localId); + }); + }); + + it("should error if email or password is missing", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ /* no email */ password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("MISSING_EMAIL"); + }); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "nosuchuser@example.com" /* no password */ }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("MISSING_PASSWORD"); + }); + }); + + it("should error if email is invalid", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "ill-formatted-email", password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("INVALID_EMAIL"); + }); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "", password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("INVALID_EMAIL"); + }); + }); + + it("should error if email is not found", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "nosuchuser@example.com", password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("EMAIL_NOT_FOUND"); + }); + }); + + it("should error if email is not found with improved email privacy enabled", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }, + "emailPrivacyConfig", + ); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "nosuchuser@example.com", password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("INVALID_LOGIN_CREDENTIALS"); + }); + }); + + it("should error if password is wrong", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await registerUser(authApi(), user); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + // Passwords are case sensitive. The uppercase one below doesn't match. + .send({ email: user.email, password: "NOTASECRET" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("INVALID_PASSWORD"); + }); + }); + + it("should error if password is wrong with improved email privacy enabled", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await updateConfig( + authApi(), + PROJECT_ID, + { + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }, + "emailPrivacyConfig", + ); + await registerUser(authApi(), user); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + // Passwords are case sensitive. The uppercase one below doesn't match. + .send({ email: user.email, password: "NOTASECRET" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).equals("INVALID_LOGIN_CREDENTIALS"); + }); + }); + + it("should error if user is disabled", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { localId } = await registerUser(authApi(), user); + await updateAccountByLocalId(authApi(), localId, { disableUser: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + + it("should return pending credential if user has MFA", async () => { + const user = { + email: "alice@example.com", + password: "notasecret", + mfaInfo: [TEST_MFA_INFO], + }; + await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("idToken"); + expect(res.body).not.to.have.property("refreshToken"); + expect(res.body.mfaPendingCredential).to.be.a("string"); + expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); + }); + }); + + it("should error if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.include("PROJECT_DISABLED"); + }); + }); + + it("should error if password sign up is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + allowPasswordSignup: false, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.include("PASSWORD_LOGIN_DISABLED"); + }); + }); + + it("should return pending credential if user has MFA and enabled on tenant projects", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + allowPasswordSignup: true, + mfaConfig: { + state: "ENABLED", + }, + }); + const user = { + email: "alice@example.com", + password: "notasecret", + mfaInfo: [TEST_MFA_INFO], + tenantId: tenant.tenantId, + }; + await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId, email: user.email, password: user.password }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("idToken"); + expect(res.body).not.to.have.property("refreshToken"); + expect(res.body.mfaPendingCredential).to.be.a("string"); + expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); + }); + }); + + describe("when blocking functions are present", () => { + afterEach(() => { + expect(nock.isDone()).to.be.true; + nock.cleanAll(); + }); + + it("should update modifiable fields before sign in", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { localId } = await registerUser(authApi(), user); + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).equals(localId); + expect(res.body.email).equals(user.email); + expect(res.body).to.have.property("registered").equals(true); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("should disable user if set", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await registerUser(authApi(), user); + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "disabled", + disabled: true, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + + it("should not trigger blocking function if user has MFA", async () => { + const user = { + email: "alice@example.com", + password: "notasecret", + mfaInfo: [TEST_MFA_INFO], + }; + await registerUser(authApi(), user); + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "disabled", + disabled: true, + }, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("idToken"); + expect(res.body).not.to.have.property("refreshToken"); + expect(res.body.mfaPendingCredential).to.be.a("string"); + expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); + }); + + // Shouldn't trigger nock calls + expect(nock.isDone()).to.be.false; + nock.cleanAll(); + }); + }); +}); diff --git a/src/emulator/auth/phone.spec.ts b/src/emulator/auth/phone.spec.ts new file mode 100644 index 00000000000..223985ab5a0 --- /dev/null +++ b/src/emulator/auth/phone.spec.ts @@ -0,0 +1,681 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; +import { FirebaseJwtPayload } from "./operations"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; +import { + expectStatusCode, + registerAnonUser, + signInWithPhoneNumber, + updateAccountByLocalId, + inspectVerificationCodes, + registerUser, + TEST_MFA_INFO, + TEST_PHONE_NUMBER, + TEST_PHONE_NUMBER_2, + enrollPhoneMfa, + registerTenant, + updateConfig, + BEFORE_CREATE_PATH, + BEFORE_CREATE_URL, + BLOCKING_FUNCTION_HOST, + DISPLAY_NAME, + PHOTO_URL, + BEFORE_SIGN_IN_PATH, + BEFORE_SIGN_IN_URL, +} from "./testing/helpers"; + +describeAuthEmulator("phone auth sign-in", ({ authApi }) => { + it("should return fake recaptcha params", async () => { + await authApi() + .get("/identitytoolkit.googleapis.com/v1/recaptchaParams") + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("recaptchaStoken").that.is.a("string"); + expect(res.body).to.have.property("recaptchaSiteKey").that.is.a("string"); + }); + }); + + it("should pretend to send a verification code via SMS", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("sessionInfo").that.is.a("string"); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + expect(codes).to.have.length(1); + expect(codes[0].phoneNumber).to.equal(phoneNumber); + expect(codes[0].sessionInfo).to.equal(sessionInfo); + expect(codes[0].code).to.be.a("string"); + }); + + it("should error when phone number is missing when calling sendVerificationCode", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ recaptchaToken: "ignored" /* no phone number */ }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error) + .to.have.property("message") + // This matches the production behavior. For some reason, it's not MISSING_PHONE_NUMBER. + .equals("INVALID_PHONE_NUMBER : Invalid format."); + }); + }); + + it("should error when phone number is invalid", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ recaptchaToken: "ignored", phoneNumber: "invalid" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error) + .to.have.property("message") + .equals("INVALID_PHONE_NUMBER : Invalid format."); + }); + }); + + it("should error on sendVerificationCode if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); + + it("should error on sendVerificationCode for tenant projects", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: false }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("UNSUPPORTED_TENANT_OPERATION"); + }); + }); + + it("should create new account by verifying phone number", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("isNewUser").equals(true); + expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); + + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.user_id).to.be.a("string"); + expect(decoded!.payload.phone_number).to.equal(phoneNumber); + expect(decoded!.payload).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("phone"); + expect(decoded!.payload.firebase.identities).to.eql({ phone: [phoneNumber] }); + }); + }); + + it("should error when sessionInfo or code is missing for signInWithPhoneNumber", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ code: "123456" /* no sessionInfo */ }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("MISSING_SESSION_INFO"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo: "something-something" /* no code */ }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("MISSING_CODE"); + }); + }); + + it("should error when sessionInfo or code is invalid", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo: "totally-invalid", code }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("INVALID_SESSION_INFO"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + // Try to send the code but with an extra "1" appended. + // This is definitely invalid since we won't have another pending code. + .send({ sessionInfo, code: code + "1" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("INVALID_CODE"); + }); + }); + + it("should error if user is disabled", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + const { localId } = await signInWithPhoneNumber(authApi(), phoneNumber); + await updateAccountByLocalId(authApi(), localId, { disableUser: true }); + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); + }); + }); + + it("should link phone number to existing account by idToken", async () => { + const { localId, idToken } = await registerAnonUser(authApi()); + + const phoneNumber = TEST_PHONE_NUMBER; + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code, idToken }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("isNewUser").equals(false); + expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); + expect(res.body.localId).to.equal(localId); + }); + }); + + it("should error if user to be linked is disabled", async () => { + const { localId, idToken } = await registerAnonUser(authApi()); + await updateAccountByLocalId(authApi(), localId, { disableUser: true }); + + const phoneNumber = TEST_PHONE_NUMBER; + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code, idToken }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); + }); + }); + + it("should error when linking phone number to existing user with MFA", async () => { + const user = { + email: "alice@example.com", + password: "notasecret", + mfaInfo: [TEST_MFA_INFO], + }; + const { idToken } = await registerUser(authApi(), user); + + const phoneNumber = TEST_PHONE_NUMBER; + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo as string; + }); + + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code, idToken }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal( + "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user.", + ); + }); + }); + + it("should error if user has MFA", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + let { idToken, localId } = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + }); + await updateAccountByLocalId(authApi(), localId, { + emailVerified: true, + phoneNumber, + }); + ({ idToken } = await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER_2)); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal( + "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user.", + ); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + expect(codes).to.be.empty; + }); + + it("should return temporaryProof if phone number already belongs to another account", async () => { + // Given a phone number that is already registered... + const phoneNumber = TEST_PHONE_NUMBER; + await signInWithPhoneNumber(authApi(), phoneNumber); + + const { idToken } = await registerAnonUser(authApi()); + + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + const temporaryProof = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code, idToken }) + .then((res) => { + expectStatusCode(200, res); + // The linking will fail, but a successful response is still returned + // with a temporaryProof (so that clients may call this API again + // without having to verify the phone number again). + expect(res.body).not.to.have.property("idToken"); + expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); + expect(res.body.temporaryProof).to.be.a("string"); + return res.body.temporaryProof; + }); + + // When called again with the returned temporaryProof, the real error + // message should now be returned. + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneNumber, temporaryProof }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PHONE_NUMBER_EXISTS"); + }); + }); + + it("should error if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); + }); + }); + + it("should error if called on tenant project", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: false }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("UNSUPPORTED_TENANT_OPERATION"); + }); + }); + + describe("when blocking functions are present", () => { + afterEach(() => { + expect(nock.isDone()).to.be.true; + nock.cleanAll(); + }); + + it("should update modifiable fields for new users", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + }, + }); + const phoneNumber = TEST_PHONE_NUMBER; + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("isNewUser").equals(true); + expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + }); + }); + + it("should update modifiable fields for existing users", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + const phoneNumber = TEST_PHONE_NUMBER; + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("isNewUser").equals(true); + expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("beforeSignIn fields should overwrite beforeCreate fields", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims", + displayName: "oldDisplayName", + photoUrl: "oldPhotoUrl", + emailVerified: false, + customClaims: { customAttribute: "oldCustom" }, + }, + }) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + const phoneNumber = TEST_PHONE_NUMBER; + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + const codes = await inspectVerificationCodes(authApi()); + const code = codes[0].code; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("isNewUser").equals(true); + expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("should disable user if set", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "disabled", + disabled: true, + }, + }); + const phoneNumber = TEST_PHONE_NUMBER; + const sessionInfo = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }) + .then((res) => { + expectStatusCode(200, res); + return res.body.sessionInfo; + }); + const codes = await inspectVerificationCodes(authApi()); + + return authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code: codes[0].code }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); + }); + }); + }); +}); diff --git a/src/test/emulators/auth/rest.spec.ts b/src/emulator/auth/rest.spec.ts similarity index 82% rename from src/test/emulators/auth/rest.spec.ts rename to src/emulator/auth/rest.spec.ts index a0d1bbb850e..5ad1e3b9a07 100644 --- a/src/test/emulators/auth/rest.spec.ts +++ b/src/emulator/auth/rest.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; -import { expectStatusCode, registerTenant, registerUser } from "./helpers"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; +import { expectStatusCode, registerTenant, registerUser } from "./testing/helpers"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; describeAuthEmulator("REST API mapping", ({ authApi }) => { it("should respond to status checks", async () => { @@ -17,6 +17,7 @@ describeAuthEmulator("REST API mapping", ({ authApi }) => { .options("/") .set("Origin", "example.com") .set("Access-Control-Request-Headers", "Authorization,X-Client-Version,X-Whatever-Header") + .set("Access-Control-Request-Private-Network", "true") .then((res) => { expectStatusCode(204, res); @@ -29,6 +30,10 @@ describeAuthEmulator("REST API mapping", ({ authApi }) => { "X-Client-Version", "X-Whatever-Header", ]); + + // Check that access-control-allow-private-network = true + // Enables accessing locahost when site is exposed via tunnel see https://github.com/firebase/firebase-tools/issues/4227 + expect(res.header["access-control-allow-private-network"]).to.eql("true"); }); }); @@ -88,6 +93,28 @@ describeAuthEmulator("authentication", ({ authApi }) => { }); }); + it("should accept API key as a query parameter", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + + it("should accept API key in HTTP Header x-goog-api-key", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("x-goog-api-key", "fake-api-key") + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + it("should ignore non-Bearer Authorization headers", async () => { await authApi() .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") @@ -138,7 +165,11 @@ describeAuthEmulator("authentication", ({ authApi }) => { .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") // This authenticates as owner of the default projectId. The exact value // and expiry don't matter -- the Emulator only checks for the format. - .set("Authorization", "Bearer ya29.AHES6ZRVmB7fkLtd1XTmq6mo0S1wqZZi3-Lh_s-6Uw7p8vtgSwg") + .set( + "Authorization", + // Not an actual token. Breaking it down to avoid linter false positives. + "Bearer ya" + "29.AHES0ZZZZZ0fff" + "ff0XXXX0mmmm0wwwww0-LL_l-0bb0b0bbbbbb", + ) .send({ // This field requires OAuth 2 and should work correctly. targetProjectId: "example2", @@ -162,7 +193,7 @@ describeAuthEmulator("authentication", ({ authApi }) => { expect(res.body.error) .to.have.property("message") .equals( - "INSUFFICIENT_PERMISSION : Only authenticated requests can specify target_project_id." + "INSUFFICIENT_PERMISSION : Only authenticated requests can specify target_project_id.", ); }); }); @@ -170,7 +201,7 @@ describeAuthEmulator("authentication", ({ authApi }) => { it("should deny requests where tenant IDs do not match in the request body and path", async () => { await authApi() .post( - "/identitytoolkit.googleapis.com/v1/projects/project-id/tenants/tenant-id/accounts:delete" + "/identitytoolkit.googleapis.com/v1/projects/project-id/tenants/tenant-id/accounts:delete", ) .set("Authorization", "Bearer owner") .send({ localId: "local-id", tenantId: "mismatching-tenant-id" }) @@ -180,12 +211,12 @@ describeAuthEmulator("authentication", ({ authApi }) => { }); }); - it("should deny requests where tenant IDs do not match in the token and path", async () => { + it("should deny requests where tenant IDs do not match in the ID token and path", async () => { const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: false, allowPasswordSignup: true, }); - const { idToken, localId } = await registerUser(authApi(), { + const { idToken } = await registerUser(authApi(), { email: "alice@example.com", password: "notasecret", tenantId: tenant.tenantId, @@ -193,7 +224,7 @@ describeAuthEmulator("authentication", ({ authApi }) => { await authApi() .post( - `/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/tenants/not-matching-tenant-id/accounts:lookup` + `/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/tenants/not-matching-tenant-id/accounts:lookup`, ) .send({ idToken }) .set("Authorization", "Bearer owner") @@ -203,12 +234,12 @@ describeAuthEmulator("authentication", ({ authApi }) => { }); }); - it("should deny requests where tenant IDs do not match in the token and request body", async () => { + it("should deny requests where tenant IDs do not match in the ID token and request body", async () => { const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: false, allowPasswordSignup: true, }); - const { idToken, localId } = await registerUser(authApi(), { + const { idToken } = await registerUser(authApi(), { email: "alice@example.com", password: "notasecret", tenantId: tenant.tenantId, diff --git a/src/emulator/auth/schema.ts b/src/emulator/auth/schema.ts index 7c3ae4d0aaf..489a3e5f141 100644 --- a/src/emulator/auth/schema.ts +++ b/src/emulator/auth/schema.ts @@ -3,416 +3,2206 @@ /* eslint-disable */ /** - * This file was auto-generated by swagger-to-ts. + * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. */ +export interface paths { + "/v1/accounts:createAuthUri": { + /** If an email identifier is specified, checks and returns if any user account is registered with the email. If there is a registered account, fetches all providers associated with the account's email. If the provider ID of an Identity Provider (IdP) is specified, creates an authorization URI for the IdP. The user can be directed to this URI to sign in with the IdP. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.createAuthUri"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:delete": { + /** Deletes a user's account. */ + post: operations["identitytoolkit.accounts.delete"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:issueSamlResponse": { + /** Experimental */ + post: operations["identitytoolkit.accounts.issueSamlResponse"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:lookup": { + /** Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria. */ + post: operations["identitytoolkit.accounts.lookup"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:resetPassword": { + /** Resets the password of an account either using an out-of-band code generated by sendOobCode or by specifying the email and password of the account to be modified. Can also check the purpose of an out-of-band code without consuming it. */ + post: operations["identitytoolkit.accounts.resetPassword"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:sendOobCode": { + /** Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it. */ + post: operations["identitytoolkit.accounts.sendOobCode"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:sendVerificationCode": { + /** Sends a SMS verification code for phone number sign-in. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.sendVerificationCode"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:signInWithCustomToken": { + /** Signs in or signs up a user by exchanging a custom Auth token. Upon a successful sign-in or sign-up, a new Identity Platform ID token and refresh token are issued for the user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.signInWithCustomToken"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:signInWithEmailLink": { + /** Signs in or signs up a user with a out-of-band code from an email link. If a user does not exist with the given email address, a user record will be created. If the sign-in succeeds, an Identity Platform ID and refresh token are issued for the authenticated user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.signInWithEmailLink"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:signInWithGameCenter": { + /** Signs in or signs up a user with iOS Game Center credentials. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. The bundle ID is required in the request header as `x-ios-bundle-identifier`. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. Apple has [deprecated the `playerID` field](https://developer.apple.com/documentation/gamekit/gkplayer/1521127-playerid/). The Apple platform Firebase SDK will use `gamePlayerID` and `teamPlayerID` from version 10.5.0 and onwards. Upgrading to SDK version 10.5.0 or later updates existing integrations that use `playerID` to instead use `gamePlayerID` and `teamPlayerID`. When making calls to `signInWithGameCenter`, you must include `playerID` along with the new fields `gamePlayerID` and `teamPlayerID` to successfully identify all existing users. Upgrading existing Game Center sign in integrations to SDK version 10.5.0 or later is irreversible. */ + post: operations["identitytoolkit.accounts.signInWithGameCenter"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:signInWithIdp": { + /** Signs in or signs up a user using credentials from an Identity Provider (IdP). This is done by manually providing an IdP credential, or by providing the authorization response obtained via the authorization request from CreateAuthUri. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. A new Identity Platform user account will be created if the user has not previously signed in to the IdP with the same account. In addition, when the "One account per email address" setting is enabled, there should not be an existing Identity Platform user account with the same email address for a new user account to be created. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.signInWithIdp"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:signInWithPassword": { + /** Signs in a user with email and password. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.signInWithPassword"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:signInWithPhoneNumber": { + /** Completes a phone number authentication attempt. If a user already exists with the given phone number, an ID token is minted for that user. Otherwise, a new user is created and associated with the phone number. This method may also be used to link a phone number to an existing user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.signInWithPhoneNumber"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:signUp": { + /** Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.signUp"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:update": { + /** Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported. */ + post: operations["identitytoolkit.accounts.update"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/accounts:verifyIosClient": { + /** Verifies an iOS client is a real iOS device. If the request is valid, a receipt will be sent in the response and a secret will be sent via Apple Push Notification Service. The client should send both of them back to certain Identity Platform APIs in a later call (for example, /accounts:sendVerificationCode), in order to verify the client. The bundle ID is required in the request header as `x-ios-bundle-identifier`. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.verifyIosClient"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts": { + /** Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.projects.accounts"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}:createSessionCookie": { + /** Creates a session cookie for the given Identity Platform ID token. The session cookie is used by the client to preserve the user's login state. */ + post: operations["identitytoolkit.projects.createSessionCookie"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}:queryAccounts": { + /** Looks up user accounts within a project or a tenant based on conditions in the request. */ + post: operations["identitytoolkit.projects.queryAccounts"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:batchCreate": { + /** Uploads multiple accounts into the Google Cloud project. If there is a problem uploading one or more of the accounts, the rest will be uploaded, and a list of the errors will be returned. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + post: operations["identitytoolkit.projects.accounts.batchCreate"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:batchDelete": { + /** Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ + post: operations["identitytoolkit.projects.accounts.batchDelete"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:batchGet": { + /** Download account information for all accounts on the project in a paginated manner. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).. Furthermore, additional permissions are needed to get password hash, password salt, and password version from accounts; otherwise these fields are redacted. */ + get: operations["identitytoolkit.projects.accounts.batchGet"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:delete": { + /** Deletes a user's account. */ + post: operations["identitytoolkit.projects.accounts.delete"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:lookup": { + /** Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria. */ + post: operations["identitytoolkit.projects.accounts.lookup"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:query": { + /** Looks up user accounts within a project or a tenant based on conditions in the request. */ + post: operations["identitytoolkit.projects.accounts.query"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:sendOobCode": { + /** Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it. */ + post: operations["identitytoolkit.projects.accounts.sendOobCode"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/accounts:update": { + /** Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported. */ + post: operations["identitytoolkit.projects.accounts.update"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts": { + /** Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.projects.tenants.accounts"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}:createSessionCookie": { + /** Creates a session cookie for the given Identity Platform ID token. The session cookie is used by the client to preserve the user's login state. */ + post: operations["identitytoolkit.projects.tenants.createSessionCookie"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:batchCreate": { + /** Uploads multiple accounts into the Google Cloud project. If there is a problem uploading one or more of the accounts, the rest will be uploaded, and a list of the errors will be returned. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + post: operations["identitytoolkit.projects.tenants.accounts.batchCreate"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:batchDelete": { + /** Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ + post: operations["identitytoolkit.projects.tenants.accounts.batchDelete"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:batchGet": { + /** Download account information for all accounts on the project in a paginated manner. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).. Furthermore, additional permissions are needed to get password hash, password salt, and password version from accounts; otherwise these fields are redacted. */ + get: operations["identitytoolkit.projects.tenants.accounts.batchGet"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:delete": { + /** Deletes a user's account. */ + post: operations["identitytoolkit.projects.tenants.accounts.delete"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:lookup": { + /** Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria. */ + post: operations["identitytoolkit.projects.tenants.accounts.lookup"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:query": { + /** Looks up user accounts within a project or a tenant based on conditions in the request. */ + post: operations["identitytoolkit.projects.tenants.accounts.query"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:sendOobCode": { + /** Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it. */ + post: operations["identitytoolkit.projects.tenants.accounts.sendOobCode"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts:update": { + /** Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported. */ + post: operations["identitytoolkit.projects.tenants.accounts.update"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/projects": { + /** Gets a project's public Identity Toolkit configuration. (Legacy) This method also supports authenticated calls from a developer to retrieve non-public configuration. */ + get: operations["identitytoolkit.getProjects"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/recaptchaParams": { + /** Gets parameters needed for generating a reCAPTCHA challenge. */ + get: operations["identitytoolkit.getRecaptchaParams"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/sessionCookiePublicKeys": { + /** Retrieves the set of public keys of the session cookie JSON Web Token (JWT) signer that can be used to validate the session cookie created through createSessionCookie. */ + get: operations["identitytoolkit.getSessionCookiePublicKeys"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/accounts:revokeToken": { + /** Revokes a user's token from an Identity Provider (IdP). This is done by manually providing an IdP credential, and the token types for revocation. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + post: operations["identitytoolkit.accounts.revokeToken"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/accounts/mfaEnrollment:finalize": { + /** Finishes enrolling a second factor for the user. */ + post: operations["identitytoolkit.accounts.mfaEnrollment.finalize"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/accounts/mfaEnrollment:start": { + /** Step one of the MFA enrollment process. In SMS case, this sends an SMS verification code to the user. */ + post: operations["identitytoolkit.accounts.mfaEnrollment.start"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/accounts/mfaEnrollment:withdraw": { + /** Revokes one second factor from the enrolled second factors for an account. */ + post: operations["identitytoolkit.accounts.mfaEnrollment.withdraw"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/accounts/mfaSignIn:finalize": { + /** Verifies the MFA challenge and performs sign-in */ + post: operations["identitytoolkit.accounts.mfaSignIn.finalize"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/accounts/mfaSignIn:start": { + /** Sends the MFA challenge */ + post: operations["identitytoolkit.accounts.mfaSignIn.start"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/defaultSupportedIdps": { + /** List all default supported Idps. */ + get: operations["identitytoolkit.defaultSupportedIdps.list"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/config": { + /** Retrieve an Identity Toolkit project configuration. */ + get: operations["identitytoolkit.projects.getConfig"]; + /** Update an Identity Toolkit project configuration. */ + patch: operations["identitytoolkit.projects.updateConfig"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/defaultSupportedIdpConfigs": { + /** List all default supported Idp configurations for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.defaultSupportedIdpConfigs.list"]; + /** Create a default supported Idp configuration for an Identity Toolkit project. */ + post: operations["identitytoolkit.projects.defaultSupportedIdpConfigs.create"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/defaultSupportedIdpConfigs/{defaultSupportedIdpConfigsId}": { + /** Retrieve a default supported Idp configuration for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.defaultSupportedIdpConfigs.get"]; + /** Delete a default supported Idp configuration for an Identity Toolkit project. */ + delete: operations["identitytoolkit.projects.defaultSupportedIdpConfigs.delete"]; + /** Update a default supported Idp configuration for an Identity Toolkit project. */ + patch: operations["identitytoolkit.projects.defaultSupportedIdpConfigs.patch"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/identityPlatform:initializeAuth": { + /** Initialize Identity Platform for a Cloud project. Identity Platform is an end-to-end authentication system for third-party users to access your apps and services. These could include mobile/web apps, games, APIs and beyond. This is the publicly available variant of EnableIdentityPlatform that is only available to billing-enabled projects. */ + post: operations["identitytoolkit.projects.identityPlatform.initializeAuth"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/inboundSamlConfigs": { + /** List all inbound SAML configurations for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.inboundSamlConfigs.list"]; + /** Create an inbound SAML configuration for an Identity Toolkit project. */ + post: operations["identitytoolkit.projects.inboundSamlConfigs.create"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/inboundSamlConfigs/{inboundSamlConfigsId}": { + /** Retrieve an inbound SAML configuration for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.inboundSamlConfigs.get"]; + /** Delete an inbound SAML configuration for an Identity Toolkit project. */ + delete: operations["identitytoolkit.projects.inboundSamlConfigs.delete"]; + /** Update an inbound SAML configuration for an Identity Toolkit project. */ + patch: operations["identitytoolkit.projects.inboundSamlConfigs.patch"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/oauthIdpConfigs": { + /** List all Oidc Idp configurations for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.oauthIdpConfigs.list"]; + /** Create an Oidc Idp configuration for an Identity Toolkit project. */ + post: operations["identitytoolkit.projects.oauthIdpConfigs.create"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/oauthIdpConfigs/{oauthIdpConfigsId}": { + /** Retrieve an Oidc Idp configuration for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.oauthIdpConfigs.get"]; + /** Delete an Oidc Idp configuration for an Identity Toolkit project. */ + delete: operations["identitytoolkit.projects.oauthIdpConfigs.delete"]; + /** Update an Oidc Idp configuration for an Identity Toolkit project. */ + patch: operations["identitytoolkit.projects.oauthIdpConfigs.patch"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants": { + /** List tenants under the given agent project. Requires read permission on the Agent project. */ + get: operations["identitytoolkit.projects.tenants.list"]; + /** Create a tenant. Requires write permission on the Agent project. */ + post: operations["identitytoolkit.projects.tenants.create"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}": { + /** Get a tenant. Requires read permission on the Tenant resource. */ + get: operations["identitytoolkit.projects.tenants.get"]; + /** Delete a tenant. Requires write permission on the Agent project. */ + delete: operations["identitytoolkit.projects.tenants.delete"]; + /** Update a tenant. Requires write permission on the Tenant resource. */ + patch: operations["identitytoolkit.projects.tenants.patch"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}:getIamPolicy": { + /** Gets the access control policy for a resource. An error is returned if the resource does not exist. An empty policy is returned if the resource exists but does not have a policy set on it. Caller must have the right Google IAM permission on the resource. */ + post: operations["identitytoolkit.projects.tenants.getIamPolicy"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}:setIamPolicy": { + /** Sets the access control policy for a resource. If the policy exists, it is replaced. Caller must have the right Google IAM permission on the resource. */ + post: operations["identitytoolkit.projects.tenants.setIamPolicy"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}:testIamPermissions": { + /** Returns the caller's permissions on a resource. An error is returned if the resource does not exist. A caller is not required to have Google IAM permission to make this request. */ + post: operations["identitytoolkit.projects.tenants.testIamPermissions"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}/defaultSupportedIdpConfigs": { + /** List all default supported Idp configurations for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.list"]; + /** Create a default supported Idp configuration for an Identity Toolkit project. */ + post: operations["identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.create"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}/defaultSupportedIdpConfigs/{defaultSupportedIdpConfigsId}": { + /** Retrieve a default supported Idp configuration for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.get"]; + /** Delete a default supported Idp configuration for an Identity Toolkit project. */ + delete: operations["identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.delete"]; + /** Update a default supported Idp configuration for an Identity Toolkit project. */ + patch: operations["identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.patch"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}/inboundSamlConfigs": { + /** List all inbound SAML configurations for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.tenants.inboundSamlConfigs.list"]; + /** Create an inbound SAML configuration for an Identity Toolkit project. */ + post: operations["identitytoolkit.projects.tenants.inboundSamlConfigs.create"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}/inboundSamlConfigs/{inboundSamlConfigsId}": { + /** Retrieve an inbound SAML configuration for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.tenants.inboundSamlConfigs.get"]; + /** Delete an inbound SAML configuration for an Identity Toolkit project. */ + delete: operations["identitytoolkit.projects.tenants.inboundSamlConfigs.delete"]; + /** Update an inbound SAML configuration for an Identity Toolkit project. */ + patch: operations["identitytoolkit.projects.tenants.inboundSamlConfigs.patch"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}/oauthIdpConfigs": { + /** List all Oidc Idp configurations for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.tenants.oauthIdpConfigs.list"]; + /** Create an Oidc Idp configuration for an Identity Toolkit project. */ + post: operations["identitytoolkit.projects.tenants.oauthIdpConfigs.create"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/projects/{targetProjectId}/tenants/{tenantId}/oauthIdpConfigs/{oauthIdpConfigsId}": { + /** Retrieve an Oidc Idp configuration for an Identity Toolkit project. */ + get: operations["identitytoolkit.projects.tenants.oauthIdpConfigs.get"]; + /** Delete an Oidc Idp configuration for an Identity Toolkit project. */ + delete: operations["identitytoolkit.projects.tenants.oauthIdpConfigs.delete"]; + /** Update an Oidc Idp configuration for an Identity Toolkit project. */ + patch: operations["identitytoolkit.projects.tenants.oauthIdpConfigs.patch"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/passwordPolicy": { + /** Gets password policy config set on the project or tenant. */ + get: operations["identitytoolkit.getPasswordPolicy"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v2/recaptchaConfig": { + /** Gets parameters needed for reCAPTCHA analysis. */ + get: operations["identitytoolkit.getRecaptchaConfig"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/v1/token": { + /** The Token Service API lets you exchange either an ID token or a refresh token for an access token and a new refresh token. You can use the access token to securely call APIs that require user authorization. */ + post: operations["securetoken.token"]; + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + }; + "/emulator/v1/projects/{targetProjectId}/accounts": { + /** Remove all accounts in the project, regardless of state. */ + delete: operations["emulator.projects.accounts.delete"]; + parameters: { + path: { + /** The ID of the Google Cloud project that the accounts belong to. */ + targetProjectId: string; + }; + }; + }; + "/emulator/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts": { + /** Remove all accounts in the project, regardless of state. */ + delete: operations["emulator.projects.accounts.delete"]; + parameters: { + path: { + /** The ID of the Google Cloud project that the accounts belong to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId: string; + }; + }; + }; + "/emulator/v1/projects/{targetProjectId}/config": { + /** Get emulator-specific configuration for the project. */ + get: operations["emulator.projects.config.get"]; + /** Update emulator-specific configuration for the project. */ + patch: operations["emulator.projects.config.update"]; + parameters: { + path: { + /** The ID of the Google Cloud project that the config belongs to. */ + targetProjectId: string; + }; + }; + }; + "/emulator/v1/projects/{targetProjectId}/oobCodes": { + /** List all pending confirmation codes for the project. */ + get: operations["emulator.projects.oobCodes.list"]; + parameters: { + path: { + /** The ID of the Google Cloud project that the confirmation codes belongs to. */ + targetProjectId: string; + }; + }; + }; + "/emulator/v1/projects/{targetProjectId}/tenants/{tenantId}/oobCodes": { + /** List all pending confirmation codes for the project. */ + get: operations["emulator.projects.oobCodes.list"]; + parameters: { + path: { + /** The ID of the Google Cloud project that the confirmation codes belongs to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId: string; + }; + }; + }; + "/emulator/v1/projects/{targetProjectId}/verificationCodes": { + /** List all pending phone verification codes for the project. */ + get: operations["emulator.projects.verificationCodes.list"]; + parameters: { + path: { + /** The ID of the Google Cloud project that the verification codes belongs to. */ + targetProjectId: string; + }; + }; + }; + "/emulator/v1/projects/{targetProjectId}/tenants/{tenantId}/verificationCodes": { + /** List all pending phone verification codes for the project. */ + get: operations["emulator.projects.verificationCodes.list"]; + parameters: { + path: { + /** The ID of the Google Cloud project that the verification codes belongs to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId: string; + }; + }; + }; +} + export interface components { schemas: { - /** - * The parameters for Argon2 hashing algorithm. - */ + /** @description The parameters for Argon2 hashing algorithm. */ GoogleCloudIdentitytoolkitV1Argon2Parameters: { /** - * The additional associated data, if provided, is appended to the hash value to provide an additional layer of security. A base64-encoded string if specified via JSON. + * Format: byte + * @description The additional associated data, if provided, is appended to the hash value to provide an additional layer of security. A base64-encoded string if specified via JSON. */ associatedData?: string; /** - * Required. The desired hash length in bytes. Minimum is 4 and maximum is 1024. + * Format: int32 + * @description Required. The desired hash length in bytes. Minimum is 4 and maximum is 1024. */ hashLengthBytes?: number; - /** - * Required. Must not be HASH_TYPE_UNSPECIFIED. - */ + /** @description Required. Must not be HASH_TYPE_UNSPECIFIED. */ hashType?: "HASH_TYPE_UNSPECIFIED" | "ARGON2_D" | "ARGON2_ID" | "ARGON2_I"; /** - * Required. The number of iterations to perform. Minimum is 1, maximum is 16. + * Format: int32 + * @description Required. The number of iterations to perform. Minimum is 1, maximum is 16. */ iterations?: number; /** - * Required. The memory cost in kibibytes. Maximum is 32768. + * Format: int32 + * @description Required. The memory cost in kibibytes. Maximum is 32768. */ memoryCostKib?: number; /** - * Required. The degree of parallelism, also called threads or lanes. Minimum is 1, maximum is 16. + * Format: int32 + * @description Required. The degree of parallelism, also called threads or lanes. Minimum is 1, maximum is 16. */ parallelism?: number; - /** - * The version of the Argon2 algorithm. This defaults to VERSION_13 if not specified. - */ + /** @description The version of the Argon2 algorithm. This defaults to VERSION_13 if not specified. */ version?: "VERSION_UNSPECIFIED" | "VERSION_10" | "VERSION_13"; }; - /** - * The information required to auto-retrieve an SMS. - */ + /** @description The information required to auto-retrieve an SMS. */ GoogleCloudIdentitytoolkitV1AutoRetrievalInfo: { - /** - * The Android app's signature hash for Google Play Service's SMS Retriever API. - */ + /** @description The Android app's signature hash for Google Play Service's SMS Retriever API. */ appSignatureHash?: string; }; - /** - * Request message for BatchDeleteAccounts. - */ + /** @description Request message for BatchDeleteAccounts. */ GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest: { - /** - * Whether to force deleting accounts that are not in disabled state. If false, only disabled accounts will be deleted, and accounts that are not disabled will be added to the `errors`. - */ + /** @description Whether to force deleting accounts that are not in disabled state. If false, only disabled accounts will be deleted, and accounts that are not disabled will be added to the `errors`. */ force?: boolean; - /** - * Required. List of user IDs to be deleted. - */ + /** @description Required. List of user IDs to be deleted. */ localIds?: string[]; - /** - * If the accounts belong to an Identity Platform tenant, the ID of the tenant. If the accounts belong to an default Identity Platform project, the field is not needed. - */ + /** @description If the accounts belong to an Identity Platform tenant, the ID of the tenant. If the accounts belong to a default Identity Platform project, the field is not needed. */ tenantId?: string; }; - /** - * Response message to BatchDeleteAccounts. - */ + /** @description Response message to BatchDeleteAccounts. */ GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse: { - /** - * Detailed error info for accounts that cannot be deleted. - */ + /** @description Detailed error info for accounts that cannot be deleted. */ errors?: components["schemas"]["GoogleCloudIdentitytoolkitV1BatchDeleteErrorInfo"][]; }; - /** - * Error info for account failed to be deleted. - */ + /** @description Error info for account failed to be deleted. */ GoogleCloudIdentitytoolkitV1BatchDeleteErrorInfo: { /** - * The index of the errored item in the original local_ids field. + * Format: int32 + * @description The index of the errored item in the original local_ids field. */ index?: number; - /** - * The corresponding user ID. - */ + /** @description The corresponding user ID. */ localId?: string; - /** - * Detailed error message. - */ + /** @description Detailed error message. */ message?: string; }; - /** - * Request message for CreateAuthUri. - */ + /** @description Request message for CreateAuthUri. */ GoogleCloudIdentitytoolkitV1CreateAuthUriRequest: { + /** @deprecated */ appId?: string; - /** - * Used for the Google provider. The type of the authentication flow to be used. If present, this should be `CODE_FLOW` to specify the authorization code flow. Otherwise, the default ID Token flow will be used. - */ + /** @description Used for the Google provider. The type of the authentication flow to be used. If present, this should be `CODE_FLOW` to specify the authorization code flow. Otherwise, the default ID Token flow will be used. */ authFlowType?: string; - /** - * An opaque string used to maintain contextual information between the authentication request and the callback from the IdP. - */ + /** @description An opaque string used to maintain contextual information between the authentication request and the callback from the IdP. */ context?: string; - /** - * A valid URL for the IdP to redirect the user back to. The URL cannot contain fragments or the reserved `state` query parameter. - */ + /** @description A valid URL for the IdP to redirect the user back to. The URL cannot contain fragments or the reserved `state` query parameter. */ continueUri?: string; - /** - * Additional customized query parameters to be added to the authorization URI. The following parameters are reserved and cannot be added: `client_id`, `response_type`, `scope`, `redirect_uri`, `state`. For the Microsoft provider, the Azure AD tenant to sign-in to can be specified in the `tenant` custom parameter. - */ + /** @description Additional customized query parameters to be added to the authorization URI. The following parameters are reserved and cannot be added: `client_id`, `response_type`, `scope`, `redirect_uri`, `state`. For the Microsoft provider, the Azure AD tenant to sign-in to can be specified in the `tenant` custom parameter. */ customParameter?: { [key: string]: string }; - /** - * Used for the Google provider. The G Suite hosted domain of the user in order to restrict sign-in to users at that domain. - */ + /** @description Used for the Google provider. The G Suite hosted domain of the user in order to restrict sign-in to users at that domain. */ hostedDomain?: string; - /** - * The email identifier of the user account to fetch associated providers for. At least one of the fields `identifier` and `provider_id` must be set. The length of the email address should be less than 256 characters and in the format of `name@domain.tld`. The email address should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. - */ + /** @description The email identifier of the user account to fetch associated providers for. At least one of the fields `identifier` and `provider_id` must be set. The length of the email address should be less than 256 characters and in the format of `name@domain.tld`. The email address should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. */ identifier?: string; + /** @deprecated */ oauthConsumerKey?: string; - /** - * Additional space-delimited OAuth 2.0 scopes specifying the scope of the authentication request with the IdP. Used for OAuth 2.0 IdPs. For the Google provider, the authorization code flow will be used if this field is set. - */ + /** @description Additional space-delimited OAuth 2.0 scopes specifying the scope of the authentication request with the IdP. Used for OAuth 2.0 IdPs. For the Google provider, the authorization code flow will be used if this field is set. */ oauthScope?: string; + /** @deprecated */ openidRealm?: string; + /** @deprecated */ otaApp?: string; - /** - * The provider ID of the IdP for the user to sign in with. This should be a provider ID enabled for sign-in, which is either from the list of [default supported IdPs](https://cloud.google.com/identity-platform/docs/reference/rest/v2/defaultSupportedIdps/list), or of the format `oidc.*` or `saml.*`. Some examples are `google.com`, `facebook.com`, `oidc.testapp`, and `saml.testapp`. At least one of the fields `identifier` and `provider_id` must be set. - */ + /** @description The provider ID of the IdP for the user to sign in with. This should be a provider ID enabled for sign-in, which is either from the list of [default supported IdPs](https://cloud.google.com/identity-platform/docs/reference/rest/v2/defaultSupportedIdps/list), or of the format `oidc.*` or `saml.*`. Some examples are `google.com`, `facebook.com`, `oidc.testapp`, and `saml.testapp`. At least one of the fields `identifier` and `provider_id` must be set. */ providerId?: string; - /** - * A session ID that can be verified against in SignInWithIdp to prevent session fixation attacks. If absent, a random string will be generated and returned as the session ID. - */ + /** @description A session ID that can be verified against in SignInWithIdp to prevent session fixation attacks. If absent, a random string will be generated and returned as the session ID. */ sessionId?: string; - /** - * The ID of the Identity Platform tenant to create an authorization URI or lookup an email identifier for. If not set, the operation will be performed in the default Identity Platform instance in the project. - */ + /** @description The ID of the Identity Platform tenant to create an authorization URI or lookup an email identifier for. If not set, the operation will be performed in the default Identity Platform instance in the project. */ tenantId?: string; }; - /** - * Response message for CreateAuthUri. - */ + /** @description Response message for CreateAuthUri. */ GoogleCloudIdentitytoolkitV1CreateAuthUriResponse: { + /** @deprecated */ allProviders?: string[]; - /** - * The authorization URI for the requested provider. Present only when a provider ID is set in the request. - */ + /** @description The authorization URI for the requested provider. Present only when a provider ID is set in the request. */ authUri?: string; - /** - * Whether a CAPTCHA is needed because there have been too many failed login attempts by the user. Present only when a registered email identifier is set in the request. - */ + /** @description Whether a CAPTCHA is needed because there have been too many failed login attempts by the user. Present only when a registered email identifier is set in the request. */ captchaRequired?: boolean; - /** - * Whether the user has previously signed in with the provider ID in the request. Present only when a registered email identifier is set in the request. - */ + /** @description Whether the user has previously signed in with the provider ID in the request. Present only when a registered email identifier is set in the request. */ forExistingProvider?: boolean; + /** @deprecated */ kind?: string; - /** - * The provider ID from the request, if provided. - */ + /** @description The provider ID from the request, if provided. */ providerId?: string; - /** - * Whether the email identifier represents an existing account. Present only when an email identifier is set in the request. - */ + /** @description Whether the email identifier represents an existing account. Present only when an email identifier is set in the request. */ registered?: boolean; - /** - * The session ID from the request, or a random string generated by CreateAuthUri if absent. It is used to prevent session fixation attacks. - */ + /** @description The session ID from the request, or a random string generated by CreateAuthUri if absent. It is used to prevent session fixation attacks. */ sessionId?: string; - /** - * The list of sign-in methods that the user has previously used. Each element is one of `password`, `emailLink`, or the provider ID of an IdP. Present only when a registered email identifier is set in the request. - */ + /** @description The list of sign-in methods that the user has previously used. Each element is one of `password`, `emailLink`, or the provider ID of an IdP. Present only when a registered email identifier is set in the request. */ signinMethods?: string[]; }; - /** - * Request message for CreateSessionCookie. - */ + /** @description Request message for CreateSessionCookie. */ GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest: { - /** - * Required. A valid Identity Platform ID token. - */ + /** @description Required. A valid Identity Platform ID token. */ idToken?: string; - /** - * The tenant ID of the Identity Platform tenant the account belongs to. - */ + /** @description The tenant ID of the Identity Platform tenant the account belongs to. */ tenantId?: string; /** - * The number of seconds until the session cookie expires. Specify a duration in seconds, between five minutes and fourteen days, inclusively. + * Format: int64 + * @description The number of seconds until the session cookie expires. Specify a duration in seconds, between five minutes and fourteen days, inclusively. */ validDuration?: string; }; - /** - * Response message for CreateSessionCookie. - */ + /** @description Response message for CreateSessionCookie. */ GoogleCloudIdentitytoolkitV1CreateSessionCookieResponse: { - /** - * The session cookie that has been created from the Identity Platform ID token specified in the request. It is in the form of a JSON Web Token (JWT). Always present. - */ + /** @description The session cookie that has been created from the Identity Platform ID token specified in the request. It is in the form of a JSON Web Token (JWT). Always present. */ sessionCookie?: string; }; - /** - * Request message for DeleteAccount. - */ + /** @description Request message for DeleteAccount. */ GoogleCloudIdentitytoolkitV1DeleteAccountRequest: { - delegatedProjectNumber?: string; /** - * The Identity Platform ID token of the account to delete. Require to be specified for requests from end users that lack Google OAuth 2.0 credential. Authenticated requests bearing a Google OAuth2 credential with proper permissions may pass local_id to specify the account to delete alternatively. + * Format: int64 + * @deprecated */ + delegatedProjectNumber?: string; + /** @description The Identity Platform ID token of the account to delete. Require to be specified for requests from end users that lack Google OAuth 2.0 credential. Authenticated requests bearing a Google OAuth2 credential with proper permissions may pass local_id to specify the account to delete alternatively. */ idToken?: string; - /** - * The ID of user account to delete. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). Requests from users lacking the credential should pass an ID token instead. - */ + /** @description The ID of user account to delete. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). Requests from users lacking the credential should pass an ID token instead. */ localId?: string; - /** - * The ID of the project which the account belongs to. Should only be specified in authenticated requests that specify local_id of an account. - */ + /** @description The ID of the project which the account belongs to. Should only be specified in authenticated requests that specify local_id of an account. */ targetProjectId?: string; - /** - * The ID of the tenant that the account belongs to, if applicable. Only require to be specified for authenticated requests bearing a Google OAuth 2.0 credential that specify local_id of an account that belongs to an Identity Platform tenant. - */ + /** @description The ID of the tenant that the account belongs to, if applicable. Only require to be specified for authenticated requests bearing a Google OAuth 2.0 credential that specify local_id of an account that belongs to an Identity Platform tenant. */ tenantId?: string; }; - /** - * Response message for DeleteAccount. - */ - GoogleCloudIdentitytoolkitV1DeleteAccountResponse: { kind?: string }; - /** - * Response message for DownloadAccount. - */ + /** @description Response message for DeleteAccount. */ + GoogleCloudIdentitytoolkitV1DeleteAccountResponse: { + /** @deprecated */ + kind?: string; + }; + /** @description Response message for DownloadAccount. */ GoogleCloudIdentitytoolkitV1DownloadAccountResponse: { + /** @deprecated */ kind?: string; - /** - * If there are more accounts to be downloaded, a token that can be passed back to DownloadAccount to get more accounts. Otherwise, this is blank. - */ + /** @description If there are more accounts to be downloaded, a token that can be passed back to DownloadAccount to get more accounts. Otherwise, this is blank. */ nextPageToken?: string; - /** - * All accounts belonging to the project/tenant limited by max_results in the request. - */ + /** @description All accounts belonging to the project/tenant limited by max_results in the request. */ users?: components["schemas"]["GoogleCloudIdentitytoolkitV1UserInfo"][]; }; - /** - * Email template - */ + /** @description Information about email MFA. */ + GoogleCloudIdentitytoolkitV1EmailInfo: { + /** @description Email address that a MFA verification should be sent to. */ + emailAddress?: string; + }; + /** @description Email template */ GoogleCloudIdentitytoolkitV1EmailTemplate: { - /** - * Email body - */ + /** @description Email body */ body?: string; - /** - * Whether the body or subject of the email is customized. - */ + /** @description Whether the body or subject of the email is customized. */ customized?: boolean; - /** - * Whether the template is disabled. If true, a default template will be used. - */ + /** @description Whether the template is disabled. If true, a default template will be used. */ disabled?: boolean; - /** - * Email body format - */ + /** @description Email body format */ format?: "EMAIL_BODY_FORMAT_UNSPECIFIED" | "PLAINTEXT" | "HTML"; - /** - * From address of the email - */ + /** @description From address of the email */ from?: string; - /** - * From display name - */ + /** @description From display name */ fromDisplayName?: string; - /** - * Local part of From address - */ + /** @description Local part of From address */ fromLocalPart?: string; - /** - * Value is in III language code format (e.g. "zh-CN", "es"). Both '-' and '_' separators are accepted. - */ + /** @description Value is in III language code format (e.g. "zh-CN", "es"). Both '-' and '_' separators are accepted. */ locale?: string; - /** - * Reply-to address - */ + /** @description Reply-to address */ replyTo?: string; - /** - * Subject of the email - */ + /** @description Subject of the email */ subject?: string; }; - /** - * Error information explaining why an account cannot be uploaded. batch upload. - */ + /** @description Error information explaining why an account cannot be uploaded. batch upload. */ GoogleCloudIdentitytoolkitV1ErrorInfo: { /** - * The index of the item, range is [0, request.size - 1] + * Format: int32 + * @description The index of the item, range is [0, request.size - 1] */ index?: number; - /** - * Detailed error message - */ + /** @description Detailed error message */ message?: string; }; - /** - * Federated user identifier at an Identity Provider. - */ + /** @description Federated user identifier at an Identity Provider. */ GoogleCloudIdentitytoolkitV1FederatedUserIdentifier: { - /** - * The ID of supported identity providers. This should be a provider ID enabled for sign-in, which is either from the list of [default supported IdPs](https://cloud.google.com/identity-platform/docs/reference/rest/v2/defaultSupportedIdps/list), or of the format `oidc.*` or `saml.*`. Some examples are `google.com`, `facebook.com`, `oidc.testapp`, and `saml.testapp`. - */ + /** @description The ID of supported identity providers. This should be a provider ID enabled for sign-in, which is either from the list of [default supported IdPs](https://cloud.google.com/identity-platform/docs/reference/rest/v2/defaultSupportedIdps/list), or of the format `oidc.*` or `saml.*`. Some examples are `google.com`, `facebook.com`, `oidc.testapp`, and `saml.testapp`. */ providerId?: string; - /** - * The user ID of the account at the third-party Identity Provider specified by `provider_id`. - */ + /** @description The user ID of the account at the third-party Identity Provider specified by `provider_id`. */ rawId?: string; }; - /** - * Request message for GetAccountInfo. - */ + /** @description Request message for GetAccountInfo. */ GoogleCloudIdentitytoolkitV1GetAccountInfoRequest: { - delegatedProjectNumber?: string; /** - * The email address of one or more accounts to fetch. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. Should only be specified by authenticated requests from a developer. + * Format: int64 + * @deprecated */ + delegatedProjectNumber?: string; + /** @description The email address of one or more accounts to fetch. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. Should only be specified by authenticated requests from a developer. */ email?: string[]; - /** - * The federated user identifier of one or more accounts to fetch. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description The federated user identifier of one or more accounts to fetch. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ federatedUserId?: components["schemas"]["GoogleCloudIdentitytoolkitV1FederatedUserIdentifier"][]; - /** - * The Identity Platform ID token of the account to fetch. Require to be specified for requests from end users. - */ + /** @description The Identity Platform ID token of the account to fetch. Require to be specified for requests from end users. */ idToken?: string; - /** - * The initial email of one or more accounts to fetch. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. Should only be specified by authenticated requests from a developer. - */ + /** @description The initial email of one or more accounts to fetch. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. Should only be specified by authenticated requests from a developer. */ initialEmail?: string[]; - /** - * The ID of one or more accounts to fetch. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description The ID of one or more accounts to fetch. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ localId?: string[]; - /** - * The phone number of one or more accounts to fetch. Should only be specified by authenticated requests from a developer and should be in E.164 format, for example, +15555555555. - */ + /** @description The phone number of one or more accounts to fetch. Should only be specified by authenticated requests from a developer and should be in E.164 format, for example, +15555555555. */ phoneNumber?: string[]; - /** - * The ID of the Google Cloud project that the account or the Identity Platform tenant specified by `tenant_id` belongs to. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description The ID of the Google Cloud project that the account or the Identity Platform tenant specified by `tenant_id` belongs to. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ targetProjectId?: string; - /** - * The ID of the tenant that the account belongs to. Should only be specified by authenticated requests from a developer. - */ + /** @description The ID of the tenant that the account belongs to. Should only be specified by authenticated requests from a developer. */ tenantId?: string; }; - /** - * Response message for GetAccountInfo. - */ + /** @description Response message for GetAccountInfo. */ GoogleCloudIdentitytoolkitV1GetAccountInfoResponse: { + /** @deprecated */ kind?: string; - /** - * The information of specific user account(s) matching the parameters in the request. - */ + /** @description The information of specific user account(s) matching the parameters in the request. */ users?: components["schemas"]["GoogleCloudIdentitytoolkitV1UserInfo"][]; }; - /** - * Request message for GetOobCode. - */ + /** @description Request message for GetOobCode. */ GoogleCloudIdentitytoolkitV1GetOobCodeRequest: { - /** - * If an associated android app can handle the OOB code, whether or not to install the android app on the device where the link is opened if the app is not already installed. - */ + /** @description If an associated android app can handle the OOB code, whether or not to install the android app on the device where the link is opened if the app is not already installed. */ androidInstallApp?: boolean; - /** - * If an associated android app can handle the OOB code, the minimum version of the app. If the version on the device is lower than this version then the user is taken to Google Play Store to upgrade the app. - */ + /** @description If an associated android app can handle the OOB code, the minimum version of the app. If the version on the device is lower than this version then the user is taken to Google Play Store to upgrade the app. */ androidMinimumVersion?: string; - /** - * If an associated android app can handle the OOB code, the Android package name of the android app that will handle the callback when this OOB code is used. This will allow the correct app to open if it is already installed, or allow Google Play Store to open to the correct app if it is not yet installed. - */ + /** @description If an associated android app can handle the OOB code, the Android package name of the android app that will handle the callback when this OOB code is used. This will allow the correct app to open if it is already installed, or allow Google Play Store to open to the correct app if it is not yet installed. */ androidPackageName?: string; - /** - * When set to true, the OOB code link will be be sent as a Universal Link or an Android App Link and will be opened by the corresponding app if installed. If not set, or set to false, the OOB code will be sent to the web widget first and then on continue will redirect to the app if installed. - */ + /** @description When set to true, the OOB code link will be be sent as a Universal Link or an Android App Link and will be opened by the corresponding app if installed. If not set, or set to false, the OOB code will be sent to the web widget first and then on continue will redirect to the app if installed. */ canHandleCodeInApp?: boolean; - /** - * For a PASSWORD_RESET request, a reCaptcha response is required when the system detects possible abuse activity. In those cases, this is the response from the reCaptcha challenge used to verify the caller. - */ + /** @description For a PASSWORD_RESET request, a reCaptcha response is required when the system detects possible abuse activity. In those cases, this is the response from the reCaptcha challenge used to verify the caller. */ captchaResp?: string; + /** @deprecated */ challenge?: string; - /** - * The Url to continue after user clicks the link sent in email. This is the url that will allow the web widget to handle the OOB code. - */ + /** @description The client type: web, Android or iOS. Required when reCAPTCHA Enterprise protection is enabled. */ + clientType?: + | "CLIENT_TYPE_UNSPECIFIED" + | "CLIENT_TYPE_WEB" + | "CLIENT_TYPE_ANDROID" + | "CLIENT_TYPE_IOS"; + /** @description The Url to continue after user clicks the link sent in email. This is the url that will allow the web widget to handle the OOB code. */ continueUrl?: string; - /** - * In order to ensure that the url used can be easily opened up in iOS or android, we create a [Firebase Dynamic Link](https://firebase.google.com/docs/dynamic-links). Most Identity Platform projects will only have one Dynamic Link domain enabled, and can leave this field blank. This field contains a specified Dynamic Link domain for projects that have multiple enabled. - */ + /** @description In order to ensure that the url used can be easily opened up in iOS or android, we create a [Firebase Dynamic Link](https://firebase.google.com/docs/dynamic-links). Most Identity Platform projects will only have one Dynamic Link domain enabled, and can leave this field blank. This field contains a specified Dynamic Link domain for projects that have multiple enabled. */ dynamicLinkDomain?: string; - /** - * The account's email address to send the OOB code to, and generally the email address of the account that needs to be updated. Required for PASSWORD_RESET, EMAIL_SIGNIN, and VERIFY_EMAIL. - */ + /** @description The account's email address to send the OOB code to, and generally the email address of the account that needs to be updated. Required for PASSWORD_RESET, EMAIL_SIGNIN, and VERIFY_EMAIL. Only required for VERIFY_AND_CHANGE_EMAIL requests when return_oob_link is set to true. In this case, it is the original email of the user. */ email?: string; - /** - * If an associated iOS app can handle the OOB code, the App Store id of this app. This will allow App Store to open to the correct app if the app is not yet installed. - */ + /** @description If an associated iOS app can handle the OOB code, the App Store id of this app. This will allow App Store to open to the correct app if the app is not yet installed. */ iOSAppStoreId?: string; - /** - * If an associated iOS app can handle the OOB code, the iOS bundle id of this app. This will allow the correct app to open if it is already installed. - */ + /** @description If an associated iOS app can handle the OOB code, the iOS bundle id of this app. This will allow the correct app to open if it is already installed. */ iOSBundleId?: string; + /** @description An ID token for the account. It is required for VERIFY_AND_CHANGE_EMAIL and VERIFY_EMAIL requests unless return_oob_link is set to true. */ idToken?: string; + /** @description The email address the account is being updated to. Required only for VERIFY_AND_CHANGE_EMAIL requests. */ newEmail?: string; - /** - * Required. The type of out-of-band (OOB) code to send. Depending on this value, other fields in this request will be required and/or have different meanings. There are 3 different OOB codes that can be sent: * PASSWORD_RESET * EMAIL_SIGNIN * VERIFY_EMAIL - */ + /** @description The reCAPTCHA version of the reCAPTCHA token in the captcha_response. */ + recaptchaVersion?: "RECAPTCHA_VERSION_UNSPECIFIED" | "RECAPTCHA_ENTERPRISE"; + /** @description Required. The type of out-of-band (OOB) code to send. Depending on this value, other fields in this request will be required and/or have different meanings. There are 4 different OOB codes that can be sent: * PASSWORD_RESET * EMAIL_SIGNIN * VERIFY_EMAIL * VERIFY_AND_CHANGE_EMAIL */ requestType?: | "OOB_REQ_TYPE_UNSPECIFIED" | "PASSWORD_RESET" @@ -423,122 +2213,78 @@ export interface components { | "EMAIL_SIGNIN" | "VERIFY_AND_CHANGE_EMAIL" | "REVERT_SECOND_FACTOR_ADDITION"; - /** - * Whether the confirmation link containing the OOB code should be returned in the response (no email is sent). Used when a developer wants to construct the email template and send it on their own. By default this is false; to specify this field, and to set it to true, it requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control) - */ + /** @description Whether the confirmation link containing the OOB code should be returned in the response (no email is sent). Used when a developer wants to construct the email template and send it on their own. By default this is false; to specify this field, and to set it to true, it requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control) */ returnOobLink?: boolean; - /** - * The Project ID of the Identity Platform project which the account belongs to. To specify this field, it requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description The Project ID of the Identity Platform project which the account belongs to. To specify this field, it requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ targetProjectId?: string; - /** - * The tenant ID of the Identity Platform tenant the account belongs to. - */ + /** @description The tenant ID of the Identity Platform tenant the account belongs to. */ tenantId?: string; - /** - * The IP address of the caller. Required only for PASSWORD_RESET requests. - */ + /** @description The IP address of the caller. Required only for PASSWORD_RESET requests. */ userIp?: string; }; - /** - * Response message for GetOobCode. - */ + /** @description Response message for GetOobCode. */ GoogleCloudIdentitytoolkitV1GetOobCodeResponse: { - /** - * If return_oob_link is false in the request, the email address the verification was sent to. - */ + /** @description If return_oob_link is false in the request, the email address the verification was sent to. */ email?: string; + /** @deprecated */ kind?: string; - /** - * If return_oob_link is true in the request, the OOB code to send. - */ + /** @description If return_oob_link is true in the request, the OOB code to send. */ oobCode?: string; - /** - * If return_oob_link is true in the request, the OOB link to be sent to the user. This returns the constructed link including [Firebase Dynamic Link](https://firebase.google.com/docs/dynamic-links) related parameters. - */ + /** @description If return_oob_link is true in the request, the OOB link to be sent to the user. This returns the constructed link including [Firebase Dynamic Link](https://firebase.google.com/docs/dynamic-links) related parameters. */ oobLink?: string; }; - /** - * Response message for GetProjectConfig. - */ + /** @description Response message for GetProjectConfig. */ GoogleCloudIdentitytoolkitV1GetProjectConfigResponse: { - /** - * Whether to allow password account sign up. This field is only returned for authenticated calls from a developer. - */ + /** @description Whether to allow password account sign up. This field is only returned for authenticated calls from a developer. */ allowPasswordUser?: boolean; - /** - * Google Cloud API key. This field is only returned for authenticated calls from a developer. - */ + /** @description Google Cloud API key. This field is only returned for authenticated calls from a developer. */ apiKey?: string; - /** - * Authorized domains for widget redirect. - */ + /** @description Authorized domains for widget redirect. */ authorizedDomains?: string[]; changeEmailTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitV1EmailTemplate"]; - /** - * The Firebase Dynamic Links domain used to construct links for redirects to native apps. - */ + /** @description The Firebase Dynamic Links domain used to construct links for redirects to native apps. */ dynamicLinksDomain?: string; - /** - * Whether anonymous user is enabled. This field is only returned for authenticated calls from a developer. - */ + /** @description Whether anonymous user is enabled. This field is only returned for authenticated calls from a developer. */ enableAnonymousUser?: boolean; - /** - * OAuth2 provider config. This field is only returned for authenticated calls from a developer. - */ + /** @description OAuth2 provider config. This field is only returned for authenticated calls from a developer. */ idpConfig?: components["schemas"]["GoogleCloudIdentitytoolkitV1IdpConfig"][]; legacyResetPasswordTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitV1EmailTemplate"]; - /** - * The project id of the retrieved configuration. - */ + /** @description The project id of the retrieved configuration. */ projectId?: string; resetPasswordTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitV1EmailTemplate"]; revertSecondFactorAdditionTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitV1EmailTemplate"]; - /** - * Whether to use email sending. This field is only returned for authenticated calls from a developer. - */ + /** @description Whether to use email sending. This field is only returned for authenticated calls from a developer. */ useEmailSending?: boolean; verifyEmailTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitV1EmailTemplate"]; }; - /** - * Response message for GetRecaptchaParam. - */ + /** @description Response message for GetRecaptchaParam. */ GoogleCloudIdentitytoolkitV1GetRecaptchaParamResponse: { + /** @deprecated */ kind?: string; - /** - * The reCAPTCHA v2 site key used to invoke the reCAPTCHA service. Always present. - */ + /** @description The producer project number used to generate PIA tokens */ + producerProjectNumber?: string; + /** @description The reCAPTCHA v2 site key used to invoke the reCAPTCHA service. Always present. */ recaptchaSiteKey?: string; + /** @deprecated */ recaptchaStoken?: string; }; - /** - * Response message for GetSessionCookiePublicKeys. - */ + /** @description Response message for GetSessionCookiePublicKeys. */ GoogleCloudIdentitytoolkitV1GetSessionCookiePublicKeysResponse: { - /** - * Public keys of the session cookie signer, formatted as [JSON Web Keys (JWK)](https://tools.ietf.org/html/rfc7517). - */ + /** @description Public keys of the session cookie signer, formatted as [JSON Web Keys (JWK)](https://tools.ietf.org/html/rfc7517). */ keys?: components["schemas"]["GoogleCloudIdentitytoolkitV1OpenIdConnectKey"][]; }; - /** - * Config of an identity provider. - */ + /** @description Config of an identity provider. */ GoogleCloudIdentitytoolkitV1IdpConfig: { - /** - * OAuth2 client ID. - */ + /** @description OAuth2 client ID. */ clientId?: string; - /** - * True if allows the user to sign in with the provider. - */ + /** @description True if allows the user to sign in with the provider. */ enabled?: boolean; /** - * Percent of users who will be prompted/redirected federated login for this IdP + * Format: int32 + * @description Percent of users who will be prompted/redirected federated login for this IdP */ experimentPercent?: number; - /** - * Name of the identity provider. - */ + /** @description Name of the identity provider. */ provider?: | "PROVIDER_UNSPECIFIED" | "MSLIVE" @@ -552,202 +2298,119 @@ export interface components { | "GOOGLE_PLAY_GAMES" | "LINKEDIN" | "IOS_GAME_CENTER"; - /** - * OAuth2 client secret. - */ + /** @description OAuth2 client secret. */ secret?: string; - /** - * Whitelisted client IDs for audience check. - */ + /** @description Whitelisted client IDs for audience check. */ whitelistedAudiences?: string[]; }; - /** - * Request message for IssueSamlResponse. - */ + /** @description Request message for IssueSamlResponse. */ GoogleCloudIdentitytoolkitV1IssueSamlResponseRequest: { - /** - * The Identity Platform ID token. It will be verified and then converted to a new SAMLResponse. - */ + /** @description The Identity Platform ID token. It will be verified and then converted to a new SAMLResponse. */ idToken?: string; - /** - * Relying Party identifier, which is the audience of issued SAMLResponse. - */ + /** @description Relying Party identifier, which is the audience of issued SAMLResponse. */ rpId?: string; - /** - * SAML app entity id specified in Google Admin Console for each app. If developers want to redirect to a third-party app rather than a G Suite app, they'll probably they need this. When it's used, we'll return a RelayState. This includes a SAMLRequest, which can be used to trigger a SP-initiated SAML flow to redirect to the real app. - */ + /** @description SAML app entity id specified in Google Admin Console for each app. If developers want to redirect to a third-party app rather than a G Suite app, they'll probably they need this. When it's used, we'll return a RelayState. This includes a SAMLRequest, which can be used to trigger a SP-initiated SAML flow to redirect to the real app. */ samlAppEntityId?: string; }; - /** - * Response for IssueSamlResponse request. - */ + /** @description Response for IssueSamlResponse request. */ GoogleCloudIdentitytoolkitV1IssueSamlResponseResponse: { - /** - * The ACS endpoint which consumes the returned SAMLResponse. - */ + /** @description The ACS endpoint which consumes the returned SAMLResponse. */ acsEndpoint?: string; - /** - * Email of the user. - */ + /** @description Email of the user. */ email?: string; - /** - * First name of the user. - */ + /** @description First name of the user. */ firstName?: string; - /** - * Whether the logged in user was created by this request. - */ + /** @description Whether the logged in user was created by this request. */ isNewUser?: boolean; - /** - * Last name of the user. - */ + /** @description Last name of the user. */ lastName?: string; - /** - * Generated RelayState. - */ + /** @description Generated RelayState. */ relayState?: string; - /** - * Signed SAMLResponse created for the Relying Party. - */ + /** @description Signed SAMLResponse created for the Relying Party. */ samlResponse?: string; }; - /** - * Information on which multi-factor authentication (MFA) providers are enabled for an account. - */ + /** @description Information on which multi-factor authentication (MFA) providers are enabled for an account. */ GoogleCloudIdentitytoolkitV1MfaEnrollment: { - /** - * Display name for this mfa option e.g. "corp cell phone". - */ + /** @description Display name for this mfa option e.g. "corp cell phone". */ displayName?: string; + emailInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1EmailInfo"]; /** - * Timestamp when the account enrolled this second factor. + * Format: google-datetime + * @description Timestamp when the account enrolled this second factor. */ enrolledAt?: string; - /** - * ID of this MFA option. - */ + /** @description ID of this MFA option. */ mfaEnrollmentId?: string; - /** - * Normally this will show the phone number associated with this enrollment. In some situations, such as after a first factor sign in, it will only show the obfuscated version of the associated phone number. - */ + /** @description Normally this will show the phone number associated with this enrollment. In some situations, such as after a first factor sign in, it will only show the obfuscated version of the associated phone number. */ phoneInfo?: string; - /** - * Output only. Unobfuscated phone_info. - */ + totpInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1TotpInfo"]; + /** @description Output only. Unobfuscated phone_info. */ unobfuscatedPhoneInfo?: string; }; GoogleCloudIdentitytoolkitV1MfaFactor: { - /** - * Display name for this mfa option e.g. "corp cell phone". - */ + /** @description Display name for this mfa option e.g. "corp cell phone". */ displayName?: string; - /** - * Phone number to receive OTP for MFA. - */ + /** @description Phone number to receive OTP for MFA. */ phoneInfo?: string; }; - /** - * Multi-factor authentication related information. - */ + /** @description Multi-factor authentication related information. */ GoogleCloudIdentitytoolkitV1MfaInfo: { - /** - * The second factors the user has enrolled. - */ + /** @description The second factors the user has enrolled. */ enrollments?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaEnrollment"][]; }; - /** - * Represents a public key of the session cookie signer, formatted as a [JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517). - */ + /** @description Represents a public key of the session cookie signer, formatted as a [JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517). */ GoogleCloudIdentitytoolkitV1OpenIdConnectKey: { - /** - * Signature algorithm. - */ + /** @description Signature algorithm. */ alg?: string; - /** - * Exponent for the RSA public key, it is represented as the base64url encoding of the value's big endian representation. - */ + /** @description Exponent for the RSA public key, it is represented as the base64url encoding of the value's big endian representation. */ e?: string; - /** - * Unique string to identify this key. - */ + /** @description Unique string to identify this key. */ kid?: string; - /** - * Key type. - */ + /** @description Key type. */ kty?: string; - /** - * Modulus for the RSA public key, it is represented as the base64url encoding of the value's big endian representation. - */ + /** @description Modulus for the RSA public key, it is represented as the base64url encoding of the value's big endian representation. */ n?: string; - /** - * Key use. - */ + /** @description Key use. */ use?: string; }; - /** - * Information about the user as provided by various Identity Providers. - */ + /** @description Information about the user as provided by various Identity Providers. */ GoogleCloudIdentitytoolkitV1ProviderUserInfo: { - /** - * The user's display name at the Identity Provider. - */ + /** @description The user's display name at the Identity Provider. */ displayName?: string; - /** - * The user's email address at the Identity Provider. - */ + /** @description The user's email address at the Identity Provider. */ email?: string; - /** - * The user's identifier at the Identity Provider. - */ + /** @description The user's identifier at the Identity Provider. */ federatedId?: string; - /** - * The user's phone number at the Identity Provider. - */ + /** @description The user's phone number at the Identity Provider. */ phoneNumber?: string; - /** - * The user's profile photo URL at the Identity Provider. - */ + /** @description The user's profile photo URL at the Identity Provider. */ photoUrl?: string; - /** - * The ID of the Identity Provider. - */ + /** @description The ID of the Identity Provider. */ providerId?: string; - /** - * The user's raw identifier directly returned from Identity Provider. - */ + /** @description The user's raw identifier directly returned from Identity Provider. */ rawId?: string; - /** - * The user's screen_name at Twitter or login name at GitHub. - */ + /** @description The user's screen_name at Twitter or login name at GitHub. */ screenName?: string; }; - /** - * Request message for QueryUserInfo. - */ + /** @description Request message for QueryUserInfo. */ GoogleCloudIdentitytoolkitV1QueryUserInfoRequest: { - /** - * Query conditions used to filter results. If more than one is passed, only the first SqlExpression is evaluated. - */ + /** @description Query conditions used to filter results. If more than one is passed, only the first SqlExpression is evaluated. */ expression?: components["schemas"]["GoogleCloudIdentitytoolkitV1SqlExpression"][]; /** - * The maximum number of accounts to return with an upper limit of __500__. Defaults to _500_. Only valid when `return_user_info` is set to `true`. + * Format: int64 + * @description The maximum number of accounts to return with an upper limit of __500__. Defaults to _500_. Only valid when `return_user_info` is set to `true`. */ limit?: string; /** - * The number of accounts to skip from the beginning of matching records. Only valid when `return_user_info` is set to `true`. + * Format: int64 + * @description The number of accounts to skip from the beginning of matching records. Only valid when `return_user_info` is set to `true`. */ offset?: string; - /** - * The order for sorting query result. Defaults to __ascending__ order. Only valid when `return_user_info` is set to `true`. - */ + /** @description The order for sorting query result. Defaults to __ascending__ order. Only valid when `return_user_info` is set to `true`. */ order?: "ORDER_UNSPECIFIED" | "ASC" | "DESC"; - /** - * If `true`, this request will return the accounts matching the query. If `false`, only the __count__ of accounts matching the query will be returned. Defaults to `true`. - */ + /** @description If `true`, this request will return the accounts matching the query. If `false`, only the __count__ of accounts matching the query will be returned. Defaults to `true`. */ returnUserInfo?: boolean; - /** - * The field to use for sorting user accounts. Defaults to `USER_ID`. Note: when `phone_number` is specified in `expression`, the result ignores the sorting. Only valid when `return_user_info` is set to `true`. - */ + /** @description The field to use for sorting user accounts. Defaults to `USER_ID`. Note: when `phone_number` is specified in `expression`, the result ignores the sorting. Only valid when `return_user_info` is set to `true`. */ sortBy?: | "SORT_BY_FIELD_UNSPECIFIED" | "USER_ID" @@ -755,57 +2418,37 @@ export interface components { | "CREATED_AT" | "LAST_LOGIN_AT" | "USER_EMAIL"; - /** - * The ID of the tenant to which the result is scoped. - */ + /** @description The ID of the tenant to which the result is scoped. */ tenantId?: string; }; - /** - * Response message for QueryUserInfo. - */ + /** @description Response message for QueryUserInfo. */ GoogleCloudIdentitytoolkitV1QueryUserInfoResponse: { /** - * If `return_user_info` in the request is true, this is the number of returned accounts in this message. Otherwise, this is the total number of accounts matching the query. + * Format: int64 + * @description If `return_user_info` in the request is true, this is the number of returned accounts in this message. Otherwise, this is the total number of accounts matching the query. */ recordsCount?: string; - /** - * If `return_user_info` in the request is true, this is the accounts matching the query. - */ + /** @description If `return_user_info` in the request is true, this is the accounts matching the query. */ userInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1UserInfo"][]; }; - /** - * Request message for ResetPassword. - */ + /** @description Request message for ResetPassword. */ GoogleCloudIdentitytoolkitV1ResetPasswordRequest: { - /** - * The email of the account to be modified. Specify this and the old password in order to change an account's password without using an out-of-band code. - */ + /** @description The email of the account to be modified. Specify this and the old password in order to change an account's password without using an out-of-band code. */ email?: string; - /** - * The new password to be set for this account. Specifying this field will result in a change to the account and consume the out-of-band code if one was specified and it was of type PASSWORD_RESET. - */ + /** @description The new password to be set for this account. Specifying this field will result in a change to the account and consume the out-of-band code if one was specified and it was of type PASSWORD_RESET. */ newPassword?: string; - /** - * The current password of the account to be modified. Specify this and email to change an account's password without using an out-of-band code. - */ + /** @description The current password of the account to be modified. Specify this and email to change an account's password without using an out-of-band code. */ oldPassword?: string; - /** - * An out-of-band (OOB) code generated by GetOobCode request. Specify only this parameter (or only this parameter and a tenant ID) to get the out-of-band code's type in the response without mutating the account's state. Only a PASSWORD_RESET out-of-band code can be consumed via this method. - */ + /** @description An out-of-band (OOB) code generated by GetOobCode request. Specify only this parameter (or only this parameter and a tenant ID) to get the out-of-band code's type in the response without mutating the account's state. Only a PASSWORD_RESET out-of-band code can be consumed via this method. */ oobCode?: string; - /** - * The tenant ID of the Identity Platform tenant the account belongs to. - */ + /** @description The tenant ID of the Identity Platform tenant the account belongs to. */ tenantId?: string; }; - /** - * Response message for ResetPassword. - */ + /** @description Response message for ResetPassword. */ GoogleCloudIdentitytoolkitV1ResetPasswordResponse: { - /** - * The email associated with the out-of-band code that was used. - */ + /** @description The email associated with the out-of-band code that was used. */ email?: string; + /** @deprecated */ kind?: string; mfaInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaEnrollment"]; newEmail?: string; @@ -820,66 +2463,48 @@ export interface components { | "VERIFY_AND_CHANGE_EMAIL" | "REVERT_SECOND_FACTOR_ADDITION"; }; - /** - * Request message for SendVerificationCode. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. - */ + /** @description Request message for SendVerificationCode. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. */ GoogleCloudIdentitytoolkitV1SendVerificationCodeRequest: { autoRetrievalInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1AutoRetrievalInfo"]; - /** - * Receipt of successful iOS app token validation. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. This should come from the response of verifyIosClient. If present, the caller should also provide the `ios_secret`, as well as a bundle ID in the `x-ios-bundle-identifier` header, which must match the bundle ID from the verifyIosClient request. - */ + /** @description Receipt of successful iOS app token validation. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. This should come from the response of verifyIosClient. If present, the caller should also provide the `ios_secret`, as well as a bundle ID in the `x-ios-bundle-identifier` header, which must match the bundle ID from the verifyIosClient request. */ iosReceipt?: string; - /** - * Secret delivered to iOS app as a push notification. Should be passed with an `ios_receipt` as well as the `x-ios-bundle-identifier` header. - */ + /** @description Secret delivered to iOS app as a push notification. Should be passed with an `ios_receipt` as well as the `x-ios-bundle-identifier` header. */ iosSecret?: string; - /** - * The phone number to send the verification code to in E.164 format. - */ + /** @description The phone number to send the verification code to in E.164 format. */ phoneNumber?: string; - /** - * Recaptcha token for app verification. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. The recaptcha should be generated by calling getRecaptchaParams and the recaptcha token will be generated on user completion of the recaptcha challenge. - */ + /** @description Android only. Used to assert application identity in place of a recaptcha token (and safety_net_token). At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, , or `play_integrity_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. A Play Integrity Token can be generated via the [PlayIntegrity API](https://developer.android.com/google/play/integrity) with applying SHA256 to the `phone_number` field as the nonce. */ + playIntegrityToken?: string; + /** @description Recaptcha token for app verification. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. The recaptcha should be generated by calling getRecaptchaParams and the recaptcha token will be generated on user completion of the recaptcha challenge. */ recaptchaToken?: string; - /** - * Android only. Used to assert application identity in place of a recaptcha token. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. A SafetyNet Token can be generated via the [SafetyNet Android Attestation API](https://developer.android.com/training/safetynet/attestation.html), with the Base64 encoding of the `phone_number` field as the nonce. - */ + /** @description Android only. Used to assert application identity in place of a recaptcha token. At least one of (`ios_receipt` and `ios_secret`), `recaptcha_token`, or `safety_net_token` must be specified to verify the verification code is being sent on behalf of a real app and not an emulator. A SafetyNet Token can be generated via the [SafetyNet Android Attestation API](https://developer.android.com/training/safetynet/attestation.html), with the Base64 encoding of the `phone_number` field as the nonce. */ safetyNetToken?: string; - /** - * Tenant ID of the Identity Platform tenant the user is signing in to. - */ + /** @description Tenant ID of the Identity Platform tenant the user is signing in to. */ tenantId?: string; }; - /** - * Response message for SendVerificationCode. - */ + /** @description Response message for SendVerificationCode. */ GoogleCloudIdentitytoolkitV1SendVerificationCodeResponse: { - /** - * Encrypted session information. This can be used in signInWithPhoneNumber to authenticate the phone number. - */ + /** @description Encrypted session information. This can be used in signInWithPhoneNumber to authenticate the phone number. */ sessionInfo?: string; }; - /** - * Request message for SetAccountInfo. - */ + /** @description Request message for SetAccountInfo. */ GoogleCloudIdentitytoolkitV1SetAccountInfoRequest: { + /** @deprecated */ captchaChallenge?: string; - /** - * The response from reCaptcha challenge. This is required when the system detects possible abuse activities. - */ + /** @description The response from reCaptcha challenge. This is required when the system detects possible abuse activities. */ captchaResponse?: string; /** - * The timestamp in milliseconds when the account was created. + * Format: int64 + * @description The timestamp in milliseconds when the account was created. */ createdAt?: string; - /** - * JSON formatted custom attributes to be stored in the Identity Platform ID token. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description JSON formatted custom attributes to be stored in the Identity Platform ID token. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ customAttributes?: string; - delegatedProjectNumber?: string; /** - * The account's attributes to be deleted. + * Format: int64 + * @deprecated */ + delegatedProjectNumber?: string; + /** @description The account's attributes to be deleted. */ deleteAttribute?: ( | "USER_ATTRIBUTE_NAME_UNSPECIFIED" | "EMAIL" @@ -889,1101 +2514,836 @@ export interface components { | "PASSWORD" | "RAW_USER_INFO" )[]; - /** - * The Identity Providers to unlink from the user's account. - */ + /** @description The Identity Providers to unlink from the user's account. */ deleteProvider?: string[]; - /** - * If true, marks the account as disabled, meaning the user will no longer be able to sign-in. - */ + /** @description If true, marks the account as disabled, meaning the user will no longer be able to sign-in. */ disableUser?: boolean; - /** - * The user's new display name to be updated in the account's attributes. The length of the display name must be less than or equal to 256 characters. - */ + /** @description The user's new display name to be updated in the account's attributes. The length of the display name must be less than or equal to 256 characters. */ displayName?: string; - /** - * The user's new email to be updated in the account's attributes. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. - */ + /** @description The user's new email to be updated in the account's attributes. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. */ email?: string; - /** - * Whether the user's email has been verified. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description Whether the user's email has been verified. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ emailVerified?: boolean; - /** - * A valid Identity Platform ID token. Required when attempting to change user-related information. - */ + /** @description A valid Identity Platform ID token. Required when attempting to change user-related information. */ idToken?: string; + /** @deprecated */ instanceId?: string; /** - * The timestamp in milliseconds when the account last logged in. + * Format: int64 + * @description The timestamp in milliseconds when the account last logged in. */ lastLoginAt?: string; linkProviderUserInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1ProviderUserInfo"]; - /** - * The ID of the user. Specifying this field requires a Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). For requests from end-users, an ID token should be passed instead. - */ + /** @description The ID of the user. Specifying this field requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). For requests from end-users, an ID token should be passed instead. */ localId?: string; mfa?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaInfo"]; - /** - * The out-of-band code to be applied on the user's account. The following out-of-band code types are supported: * VERIFY_EMAIL * RECOVER_EMAIL * REVERT_SECOND_FACTOR_ADDITION * VERIFY_AND_CHANGE_EMAIL - */ + /** @description The out-of-band code to be applied on the user's account. The following out-of-band code types are supported: * VERIFY_EMAIL * RECOVER_EMAIL * REVERT_SECOND_FACTOR_ADDITION * VERIFY_AND_CHANGE_EMAIL */ oobCode?: string; - /** - * The user's new password to be updated in the account's attributes. The password must be at least 6 characters long. - */ + /** @description The user's new password to be updated in the account's attributes. The password must be at least 6 characters long. */ password?: string; - /** - * The phone number to be updated in the account's attributes. - */ + /** @description The phone number to be updated in the account's attributes. */ phoneNumber?: string; - /** - * The user's new photo URL for the account's profile photo to be updated in the account's attributes. The length of the URL must be less than or equal to 2048 characters. - */ + /** @description The user's new photo URL for the account's profile photo to be updated in the account's attributes. The length of the URL must be less than or equal to 2048 characters. */ photoUrl?: string; - /** - * The Identity Providers that the account should be associated with. - */ + /** @description The Identity Providers that the account should be associated with. */ provider?: string[]; - /** - * Whether or not to return an ID and refresh token. Should always be true. - */ + /** @description Whether or not to return an ID and refresh token. Should always be true. */ returnSecureToken?: boolean; - /** - * The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper permissions (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead. - */ + /** @description The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead. */ targetProjectId?: string; - /** - * The tenant ID of the Identity Platform tenant that the account belongs to. Requests from end users should pass an Identity Platform ID token rather than setting this field. - */ + /** @description The tenant ID of the Identity Platform tenant that the account belongs to. Requests from end users should pass an Identity Platform ID token rather than setting this field. */ tenantId?: string; - /** - * Whether the account should be restricted to only using federated login. - */ + /** @description Whether the account should be restricted to only using federated login. */ upgradeToFederatedLogin?: boolean; /** - * Specifies the minimum timestamp in seconds for an Identity Platform ID token to be considered valid. + * Format: int64 + * @description Specifies the minimum timestamp in seconds for an Identity Platform ID token to be considered valid. */ validSince?: string; }; - /** - * Response message for SetAccountInfo - */ + /** @description Response message for SetAccountInfo */ GoogleCloudIdentitytoolkitV1SetAccountInfoResponse: { + /** + * @deprecated + * @description The account's display name. + */ displayName?: string; - email?: string; /** - * Whether the account's email has been verified. + * @deprecated + * @description The account's email address. */ + email?: string; + /** @description Whether the account's email has been verified. */ emailVerified?: boolean; /** - * The number of seconds until the Identity Platform ID token expires. + * Format: int64 + * @description The number of seconds until the Identity Platform ID token expires. */ expiresIn?: string; - /** - * An Identity Platform ID token for the account. This is used for legacy user sign up. - */ + /** @description An Identity Platform ID token for the account. This is used for legacy user sign up. */ idToken?: string; + /** @deprecated */ kind?: string; - /** - * The ID of the authenticated user. - */ + /** @description The ID of the authenticated user. */ localId?: string; + /** @description The new email that has been set on the user's account attributes. */ + newEmail?: string; /** - * The new email that has been set on the user's account attributes. + * @deprecated + * @description Deprecated. No actual password hash is currently returned. */ - newEmail?: string; passwordHash?: string; - photoUrl?: string; /** - * The linked Identity Providers on the account. + * @deprecated + * @description The user's photo URL for the account's profile photo. */ + photoUrl?: string; + /** @description The linked Identity Providers on the account. */ providerUserInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1ProviderUserInfo"][]; - /** - * A refresh token for the account. This is used for legacy user sign up. - */ + /** @description A refresh token for the account. This is used for legacy user sign up. */ refreshToken?: string; }; - /** - * Request message for SignInWithCustomToken. - */ + /** @description Request message for SignInWithCustomToken. */ GoogleCloudIdentitytoolkitV1SignInWithCustomTokenRequest: { - delegatedProjectNumber?: string; - instanceId?: string; /** - * Should always be true. + * Format: int64 + * @deprecated */ + delegatedProjectNumber?: string; + /** @deprecated */ + instanceId?: string; + /** @description Should always be true. */ returnSecureToken?: boolean; - /** - * The ID of the Identity Platform tenant the user is signing in to. If present, the ID should match the tenant_id in the token. - */ + /** @description The ID of the Identity Platform tenant the user is signing in to. If present, the ID should match the tenant_id in the token. */ tenantId?: string; - /** - * Required. The custom Auth token asserted by the developer. The token should be a [JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) that includes the claims listed in the [API reference](https://cloud.google.com/identity-platform/docs/reference/rest/client/) under the "Custom Token Claims" section. - */ + /** @description Required. The custom Auth token asserted by the developer. The token should be a [JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) that includes the claims listed in the [API reference](https://cloud.google.com/identity-platform/docs/reference/rest/client/) under the "Custom Token Claims" section. */ token?: string; }; - /** - * Response message for SignInWithCustomToken. - */ + /** @description Response message for SignInWithCustomToken. */ GoogleCloudIdentitytoolkitV1SignInWithCustomTokenResponse: { /** - * The number of seconds until the ID token expires. + * Format: int64 + * @description The number of seconds until the ID token expires. */ expiresIn?: string; - /** - * An Identity Platform ID token for the authenticated user. - */ + /** @description An Identity Platform ID token for the authenticated user. */ idToken?: string; - /** - * Whether the authenticated user was created by this request. - */ + /** @description Whether the authenticated user was created by this request. */ isNewUser?: boolean; + /** @deprecated */ kind?: string; - /** - * An Identity Platform refresh token for the authenticated user. - */ + /** @description An Identity Platform refresh token for the authenticated user. */ refreshToken?: string; }; - /** - * Request message for SignInWithEmailLink - */ + /** @description Request message for SignInWithEmailLink */ GoogleCloudIdentitytoolkitV1SignInWithEmailLinkRequest: { - /** - * Required. The email address the sign-in link was sent to. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. - */ + /** @description Required. The email address the sign-in link was sent to. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. */ email?: string; - /** - * A valid ID token for an Identity Platform account. If passed, this request will link the email address to the user represented by this ID token and enable sign-in with email link on the account for the future. - */ + /** @description A valid ID token for an Identity Platform account. If passed, this request will link the email address to the user represented by this ID token and enable sign-in with email link on the account for the future. */ idToken?: string; - /** - * Required. The out-of-band code from the email link. - */ + /** @description Required. The out-of-band code from the email link. */ oobCode?: string; - /** - * The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. */ tenantId?: string; }; - /** - * Response message for SignInWithEmailLink. - */ + /** @description Response message for SignInWithEmailLink. */ GoogleCloudIdentitytoolkitV1SignInWithEmailLinkResponse: { - /** - * The email the user signed in with. Always present in the response. - */ + /** @description The email the user signed in with. Always present in the response. */ email?: string; /** - * The number of seconds until the ID token expires. + * Format: int64 + * @description The number of seconds until the ID token expires. */ expiresIn?: string; - /** - * An Identity Platform ID token for the authenticated user. - */ + /** @description An Identity Platform ID token for the authenticated user. */ idToken?: string; - /** - * Whether the authenticated user was created by this request. - */ + /** @description Whether the authenticated user was created by this request. */ isNewUser?: boolean; + /** @deprecated */ kind?: string; - /** - * The ID of the authenticated user. Always present in the response. - */ + /** @description The ID of the authenticated user. Always present in the response. */ localId?: string; - /** - * Info on which multi-factor authentication providers are enabled. Present if the user needs to complete the sign-in using multi-factor authentication. - */ + /** @description Info on which multi-factor authentication providers are enabled. Present if the user needs to complete the sign-in using multi-factor authentication. */ mfaInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaEnrollment"][]; - /** - * An opaque string that functions as proof that the user has successfully passed the first factor check. - */ + /** @description An opaque string that functions as proof that the user has successfully passed the first factor check. */ mfaPendingCredential?: string; - /** - * Refresh token for the authenticated user. - */ + /** @description Refresh token for the authenticated user. */ refreshToken?: string; }; - /** - * Request message for SignInWithGameCenter - */ + /** @description Request message for SignInWithGameCenter */ GoogleCloudIdentitytoolkitV1SignInWithGameCenterRequest: { - /** - * The user's Game Center display name. - */ + /** @description The user's Game Center display name. */ displayName?: string; - /** - * A valid ID token for an Identity Platform account. If present, this request will link the Game Center player ID to the account represented by this ID token. - */ + /** @description The user's Game Center game player ID. A unique identifier for a player of the game. https://developer.apple.com/documentation/gamekit/gkplayer/3113960-gameplayerid */ + gamePlayerId?: string; + /** @description A valid ID token for an Identity Platform account. If present, this request will link the Game Center player ID to the account represented by this ID token. */ idToken?: string; - /** - * Required. The user's Game Center player ID. - */ + /** @description Required. The user's Game Center player ID. Deprecated by Apple. Pass `playerID` along with `gamePlayerID` and `teamPlayerID` to initiate the migration of a user's Game Center player ID to `gamePlayerID`. */ playerId?: string; - /** - * Required. The URL to fetch the Apple public key in order to verify the given signature is signed by Apple. - */ + /** @description Required. The URL to fetch the Apple public key in order to verify the given signature is signed by Apple. */ publicKeyUrl?: string; - /** - * Required. A random string used to generate the given signature. - */ + /** @description Required. A random string used to generate the given signature. */ salt?: string; - /** - * Required. The verification signature data generated by Apple. - */ + /** @description Required. The verification signature data generated by Apple. */ signature?: string; - /** - * The ID of the Identity Platform tenant the user is signing in to. - */ + /** @description The user's Game Center team player ID. A unique identifier for a player of all the games that you distribute using your developer account. https://developer.apple.com/documentation/gamekit/gkplayer/3174857-teamplayerid */ + teamPlayerId?: string; + /** @description The ID of the Identity Platform tenant the user is signing in to. */ tenantId?: string; /** - * Required. The time when the signature was created by Apple, in milliseconds since the epoch. + * Format: int64 + * @description Required. The time when the signature was created by Apple, in milliseconds since the epoch. */ timestamp?: string; }; - /** - * Response message for SignInWithGameCenter - */ + /** @description Response message for SignInWithGameCenter */ GoogleCloudIdentitytoolkitV1SignInWithGameCenterResponse: { - /** - * Display name of the authenticated user. - */ + /** @description Display name of the authenticated user. */ displayName?: string; /** - * The number of seconds until the ID token expires. + * Format: int64 + * @description The number of seconds until the ID token expires. */ expiresIn?: string; - /** - * An Identity Platform ID token for the authenticated user. - */ + /** @description The user's Game Center game player ID. A unique identifier for a player of the game. https://developer.apple.com/documentation/gamekit/gkplayer/3113960-gameplayerid */ + gamePlayerId?: string; + /** @description An Identity Platform ID token for the authenticated user. */ idToken?: string; - /** - * Whether the logged in user was created by this request. - */ + /** @description Whether the logged in user was created by this request. */ isNewUser?: boolean; - /** - * The ID of the authenticated user. Always present in the response. - */ + /** @description The ID of the authenticated user. Always present in the response. */ localId?: string; - /** - * The user's Game Center player ID. - */ + /** @description The user's Game Center player ID. Pass `playerID` along with `gamePlayerID` and `teamPlayerID` to initiate the migration of a user's Game Center player ID to `gamePlayerID`. */ playerId?: string; - /** - * An Identity Platform refresh token for the authenticated user. - */ + /** @description An Identity Platform refresh token for the authenticated user. */ refreshToken?: string; + /** @description The user's Game Center team player ID. A unique identifier for a player of all the games that you distribute using your developer account. https://developer.apple.com/documentation/gamekit/gkplayer/3174857-teamplayerid */ + teamPlayerId?: string; }; - /** - * Request message for SignInWithIdp. - */ + /** @description Request message for SignInWithIdp. */ GoogleCloudIdentitytoolkitV1SignInWithIdpRequest: { + /** @deprecated */ autoCreate?: boolean; - delegatedProjectNumber?: string; /** - * A valid Identity Platform ID token. If passed, the user's account at the IdP will be linked to the account represented by this ID token. + * Format: int64 + * @deprecated */ + delegatedProjectNumber?: string; + /** @description A valid Identity Platform ID token. If passed, the user's account at the IdP will be linked to the account represented by this ID token. */ idToken?: string; + /** @deprecated */ pendingIdToken?: string; - /** - * An opaque string from a previous SignInWithIdp response. If set, it can be used to repeat the sign-in operation from the previous SignInWithIdp operation. - */ + /** @description An opaque string from a previous SignInWithIdp response. If set, it can be used to repeat the sign-in operation from the previous SignInWithIdp operation. This may be present if the user needs to confirm their account information as part of a previous federated login attempt, or perform account linking. */ pendingToken?: string; - /** - * If the user is signing in with an authorization response obtained via a previous CreateAuthUri authorization request, this is the body of the HTTP POST callback from the IdP, if present. Otherwise, if the user is signing in with a manually provided IdP credential, this should be a URL-encoded form that contains the credential (e.g. an ID token or access token for OAuth 2.0 IdPs) and the provider ID of the IdP that issued the credential. For example, if the user is signing in to the Google provider using a Google ID token, this should be set to `id_token=[GOOGLE_ID_TOKEN]&providerId=google.com`, where `[GOOGLE_ID_TOKEN]` should be replaced with the Google ID token. If the user is signing in to the Facebook provider using a Facebook authentication token, this should be set to `id_token=[FACEBOOK_AUTHENTICATION_TOKEN]&providerId=facebook.com&nonce= [NONCE]`, where `[FACEBOOK_AUTHENTICATION_TOKEN]` should be replaced with the Facebook authentication token. Nonce is required for validating the token. The request will fail if no nonce is provided. If the user is signing in to the Facebook provider using a Facebook access token, this should be set to `access_token=[FACEBOOK_ACCESS_TOKEN]&providerId=facebook.com`, where `[FACEBOOK_ACCESS_TOKEN]` should be replaced with the Facebook access token. If the user is signing in to the Twitter provider using a Twitter OAuth 1.0 credential, this should be set to `access_token=[TWITTER_ACCESS_TOKEN]&oauth_token_secret=[TWITTER_TOKEN_SECRET]&providerId=twitter.com`, where `[TWITTER_ACCESS_TOKEN]` and `[TWITTER_TOKEN_SECRET]` should be replaced with the Twitter OAuth access token and Twitter OAuth token secret respectively. - */ + /** @description If the user is signing in with an authorization response obtained via a previous CreateAuthUri authorization request, this is the body of the HTTP POST callback from the IdP, if present. Otherwise, if the user is signing in with a manually provided IdP credential, this should be a URL-encoded form that contains the credential (e.g. an ID token or access token for OAuth 2.0 IdPs) and the provider ID of the IdP that issued the credential. For example, if the user is signing in to the Google provider using a Google ID token, this should be set to id_token`=[GOOGLE_ID_TOKEN]&providerId=google.com`, where `[GOOGLE_ID_TOKEN]` should be replaced with the Google ID token. If the user is signing in to the Facebook provider using a Facebook authentication token, this should be set to id_token`=[FACEBOOK_AUTHENTICATION_TOKEN]&providerId=facebook. com&nonce= [NONCE]`, where `[FACEBOOK_AUTHENTICATION_TOKEN]` should be replaced with the Facebook authentication token. Nonce is required for validating the token. The request will fail if no nonce is provided. If the user is signing in to the Facebook provider using a Facebook access token, this should be set to access_token`=[FACEBOOK_ACCESS_TOKEN]&providerId=facebook. com`, where `[FACEBOOK_ACCESS_TOKEN]` should be replaced with the Facebook access token. If the user is signing in to the Twitter provider using a Twitter OAuth 1.0 credential, this should be set to access_token`=[TWITTER_ACCESS_TOKEN]&oauth_token_secret= [TWITTER_TOKEN_SECRET]&providerId=twitter.com`, where `[TWITTER_ACCESS_TOKEN]` and `[TWITTER_TOKEN_SECRET]` should be replaced with the Twitter OAuth access token and Twitter OAuth token secret respectively. */ postBody?: string; - /** - * Required. The URL to which the IdP redirects the user back. This can be set to `http://localhost` if the user is signing in with a manually provided IdP credential. - */ + /** @description Required. The URL to which the IdP redirects the user back. This can be set to `http://localhost` if the user is signing in with a manually provided IdP credential. */ requestUri?: string; - /** - * Whether or not to return OAuth credentials from the IdP on the following errors: `FEDERATED_USER_ID_ALREADY_LINKED` and `EMAIL_EXISTS`. - */ + /** @description Whether or not to return OAuth credentials from the IdP on the following errors: `FEDERATED_USER_ID_ALREADY_LINKED` and `EMAIL_EXISTS`. */ returnIdpCredential?: boolean; - /** - * Whether or not to return the OAuth refresh token from the IdP, if available. - */ + /** @description Whether or not to return the OAuth refresh token from the IdP, if available. */ returnRefreshToken?: boolean; - /** - * Should always be true. - */ + /** @description Should always be true. */ returnSecureToken?: boolean; - /** - * The session ID returned from a previous CreateAuthUri call. This field is verified against that session ID to prevent session fixation attacks. Required if the user is signing in with an authorization response from a previous CreateAuthUri authorization request. - */ + /** @description The session ID returned from a previous CreateAuthUri call. This field is verified against that session ID to prevent session fixation attacks. Required if the user is signing in with an authorization response from a previous CreateAuthUri authorization request. */ sessionId?: string; - /** - * The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. */ tenantId?: string; }; - /** - * Response message for SignInWithIdp. - */ + /** @description Response message for SignInWithIdp. */ GoogleCloudIdentitytoolkitV1SignInWithIdpResponse: { - /** - * The opaque string set in CreateAuthUri that is used to maintain contextual information between the authentication request and the callback from the IdP. - */ + /** @description The opaque string set in CreateAuthUri that is used to maintain contextual information between the authentication request and the callback from the IdP. */ context?: string; - /** - * The date of birth for the user's account at the IdP. - */ + /** @description The date of birth for the user's account at the IdP. */ dateOfBirth?: string; - /** - * The display name for the user's account at the IdP. - */ + /** @description The display name for the user's account at the IdP. */ displayName?: string; - /** - * The email address of the user's account at the IdP. - */ + /** @description The email address of the user's account at the IdP. */ email?: string; - /** - * Whether or not there is an existing Identity Platform user account with the same email address but linked to a different account at the same IdP. Only present if the "One account per email address" setting is enabled and the email address at the IdP is verified. - */ + /** @description Whether or not there is an existing Identity Platform user account with the same email address but linked to a different account at the same IdP. Only present if the "One account per email address" setting is enabled and the email address at the IdP is verified. */ emailRecycled?: boolean; - /** - * Whether the user account's email address is verified. - */ + /** @description Whether the user account's email address is verified. */ emailVerified?: boolean; - /** - * The error message returned if `return_idp_credential` is set to `true` and either the `FEDERATED_USER_ID_ALREADY_LINKED` or `EMAIL_EXISTS` error is encountered. This field's value is either `FEDERATED_USER_ID_ALREADY_LINKED` or `EMAIL_EXISTS`. - */ + /** @description The error message returned if `return_idp_credential` is set to `true` and either the `FEDERATED_USER_ID_ALREADY_LINKED` or `EMAIL_EXISTS` error is encountered. This field's value is either `FEDERATED_USER_ID_ALREADY_LINKED` or `EMAIL_EXISTS`. */ errorMessage?: string; /** - * The number of seconds until the Identity Platform ID token expires. + * Format: int64 + * @description The number of seconds until the Identity Platform ID token expires. */ expiresIn?: string; - /** - * The user's account ID at the IdP. Always present in the response. - */ + /** @description The user's account ID at the IdP. Always present in the response. */ federatedId?: string; - /** - * The first name for the user's account at the IdP. - */ + /** @description The first name for the user's account at the IdP. */ firstName?: string; - /** - * The full name for the user's account at the IdP. - */ + /** @description The full name for the user's account at the IdP. */ fullName?: string; - /** - * An Identity Platform ID token for the authenticated user. - */ + /** @description An Identity Platform ID token for the authenticated user. */ idToken?: string; + /** @deprecated */ inputEmail?: string; - /** - * Whether or not a new Identity Platform account was created for the authenticated user. - */ + /** @description Whether or not a new Identity Platform account was created for the authenticated user. */ isNewUser?: boolean; + /** @deprecated */ kind?: string; - /** - * The language preference for the user's account at the IdP. - */ + /** @description The language preference for the user's account at the IdP. */ language?: string; - /** - * The last name for the user's account at the IdP. - */ + /** @description The last name for the user's account at the IdP. */ lastName?: string; - /** - * The ID of the authenticated Identity Platform user. Always present in the response. - */ + /** @description The ID of the authenticated Identity Platform user. Always present in the response. */ localId?: string; - /** - * Info on which multi-factor authentication providers are enabled for the account. Present if the user needs to complete the sign-in using multi-factor authentication. - */ + /** @description Info on which multi-factor authentication providers are enabled for the account. Present if the user needs to complete the sign-in using multi-factor authentication. */ mfaInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaEnrollment"][]; - /** - * An opaque string that functions as proof that the user has successfully passed the first factor authentication. - */ + /** @description An opaque string that functions as proof that the user has successfully passed the first factor authentication. */ mfaPendingCredential?: string; - /** - * Whether or not there is an existing Identity Platform user account with the same email address as the current account signed in at the IdP, and the account's email addresss is not verified at the IdP. The user will need to sign in to the existing Identity Platform account and then link the current credential from the IdP to it. Only present if the "One account per email address" setting is enabled. - */ + /** @description Whether or not there is an existing Identity Platform user account with the same email address as the current account signed in at the IdP, and the account's email addresss is not verified at the IdP. The user will need to sign in to the existing Identity Platform account and then link the current credential from the IdP to it. Only present if the "One account per email address" setting is enabled. */ needConfirmation?: boolean; + /** @deprecated */ needEmail?: boolean; - /** - * The nickname for the user's account at the IdP. - */ + /** @description The nickname for the user's account at the IdP. */ nickName?: string; - /** - * The OAuth access token from the IdP, if available. - */ + /** @description The OAuth access token from the IdP, if available. */ oauthAccessToken?: string; - /** - * The OAuth 2.0 authorization code, if available. Only present for the Google provider. - */ + /** @description The OAuth 2.0 authorization code, if available. Only present for the Google provider. */ oauthAuthorizationCode?: string; /** - * The number of seconds until the OAuth access token from the IdP expires. + * Format: int32 + * @description The number of seconds until the OAuth access token from the IdP expires. */ oauthExpireIn?: number; - /** - * The OpenID Connect ID token from the IdP, if available. - */ + /** @description The OpenID Connect ID token from the IdP, if available. */ oauthIdToken?: string; - /** - * The OAuth 2.0 refresh token from the IdP, if available and `return_refresh_token` is set to `true`. - */ + /** @description The OAuth 2.0 refresh token from the IdP, if available and `return_refresh_token` is set to `true`. */ oauthRefreshToken?: string; - /** - * The OAuth 1.0 token secret from the IdP, if available. Only present for the Twitter provider. - */ + /** @description The OAuth 1.0 token secret from the IdP, if available. Only present for the Twitter provider. */ oauthTokenSecret?: string; - /** - * The main (top-level) email address of the user's Identity Platform account, if different from the email address at the IdP. Only present if the "One account per email address" setting is enabled. - */ + /** @description The main (top-level) email address of the user's Identity Platform account, if different from the email address at the IdP. Only present if the "One account per email address" setting is enabled. */ originalEmail?: string; - /** - * An opaque string that can be used as a credential from the IdP the user is signing into. The pending token obtained here can be set in a future SignInWithIdp request to sign the same user in with the IdP again. - */ + /** @description An opaque string that can be used as a credential from the IdP the user is signing into. The pending token obtained here can be set in a future SignInWithIdp request to sign the same user in with the IdP again. */ pendingToken?: string; - /** - * The URL of the user's profile picture at the IdP. - */ + /** @description The URL of the user's profile picture at the IdP. */ photoUrl?: string; - /** - * The provider ID of the IdP that the user is signing in to. Always present in the response. - */ + /** @description The provider ID of the IdP that the user is signing in to. Always present in the response. */ providerId?: string; - /** - * The stringified JSON response containing all the data corresponding to the user's account at the IdP. - */ + /** @description The stringified JSON response containing all the data corresponding to the user's account at the IdP. */ rawUserInfo?: string; - /** - * An Identity Platform refresh token for the authenticated user. - */ + /** @description An Identity Platform refresh token for the authenticated user. */ refreshToken?: string; - /** - * The screen name for the user's account at the Twitter IdP or the login name for the user's account at the GitHub IdP. - */ + /** @description The screen name for the user's account at the Twitter IdP or the login name for the user's account at the GitHub IdP. */ screenName?: string; - /** - * The value of the `tenant_id` field in the request. - */ + /** @description The value of the `tenant_id` field in the request. */ tenantId?: string; - /** - * The time zone for the user's account at the IdP. - */ + /** @description The time zone for the user's account at the IdP. */ timeZone?: string; - /** - * A list of provider IDs that the user can sign in to in order to resolve a `need_confirmation` error. Only present if `need_confirmation` is set to `true`. - */ + /** @description A list of provider IDs that the user can sign in to in order to resolve a `need_confirmation` error. Only present if `need_confirmation` is set to `true`. */ verifiedProvider?: string[]; }; - /** - * Request message for SignInWithPassword. - */ + /** @description Request message for SignInWithPassword. */ GoogleCloudIdentitytoolkitV1SignInWithPasswordRequest: { + /** @deprecated */ captchaChallenge?: string; - /** - * The response from a reCaptcha challenge. A recaptcha response is required when the service detects possible abuse activity. - */ + /** @description The reCAPTCHA token provided by the reCAPTCHA client-side integration. reCAPTCHA Enterprise uses it for risk assessment. Required when reCAPTCHA Enterprise is enabled. */ captchaResponse?: string; - delegatedProjectNumber?: string; + /** @description The client type, web, android or ios. Required when reCAPTCHA Enterprise is enabled. */ + clientType?: + | "CLIENT_TYPE_UNSPECIFIED" + | "CLIENT_TYPE_WEB" + | "CLIENT_TYPE_ANDROID" + | "CLIENT_TYPE_IOS"; /** - * Required. The email the user is signing in with. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. + * Format: int64 + * @deprecated */ + delegatedProjectNumber?: string; + /** @description Required. The email the user is signing in with. The length of email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. */ email?: string; + /** @deprecated */ idToken?: string; + /** @deprecated */ instanceId?: string; - /** - * Required. The password the user provides to sign in to the account. - */ + /** @description Required. The password the user provides to sign in to the account. */ password?: string; + /** @deprecated */ pendingIdToken?: string; - /** - * Should always be true. - */ + /** @description The reCAPTCHA version of the reCAPTCHA token in the captcha_response. */ + recaptchaVersion?: "RECAPTCHA_VERSION_UNSPECIFIED" | "RECAPTCHA_ENTERPRISE"; + /** @description Should always be true. */ returnSecureToken?: boolean; - /** - * The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform instance in the project. - */ + /** @description The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform instance in the project. */ tenantId?: string; }; - /** - * Response message for SignInWithPassword. - */ + /** @description Response message for SignInWithPassword. */ GoogleCloudIdentitytoolkitV1SignInWithPasswordResponse: { - /** - * The user's display name stored in the account's attributes. - */ + /** @description The user's display name stored in the account's attributes. */ displayName?: string; - /** - * The email of the authenticated user. Always present in the response. - */ + /** @description The email of the authenticated user. Always present in the response. */ email?: string; /** - * The number of seconds until the Identity Platform ID token expires. + * Format: int64 + * @description The number of seconds until the Identity Platform ID token expires. */ expiresIn?: string; - /** - * An Identity Platform ID token for the authenticated user. - */ + /** @description An Identity Platform ID token for the authenticated user. */ idToken?: string; + /** @deprecated */ kind?: string; - /** - * The ID of the authenticated user. Always present in the response. - */ + /** @description The ID of the authenticated user. Always present in the response. */ localId?: string; - /** - * Info on which multi-factor authentication providers are enabled for the account. Present if the user needs to complete the sign-in using multi-factor authentication. - */ + /** @description Info on which multi-factor authentication providers are enabled for the account. Present if the user needs to complete the sign-in using multi-factor authentication. */ mfaInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaEnrollment"][]; - /** - * An opaque string that functions as proof that the user has successfully passed the first factor authentication. - */ + /** @description An opaque string that functions as proof that the user has successfully passed the first factor authentication. */ mfaPendingCredential?: string; /** - * The OAuth2 access token. + * @deprecated + * @description The OAuth2 access token. */ oauthAccessToken?: string; + /** @deprecated */ oauthAuthorizationCode?: string; /** - * The access token expiration time in seconds. + * Format: int32 + * @deprecated + * @description The access token expiration time in seconds. */ oauthExpireIn?: number; - /** - * The user's profile picture stored in the account's attributes. - */ + /** @description The user's profile picture stored in the account's attributes. */ profilePicture?: string; - /** - * An Identity Platform refresh token for the authenticated user. - */ + /** @description An Identity Platform refresh token for the authenticated user. */ refreshToken?: string; /** - * Whether the email is for an existing account. Always true. + * @deprecated + * @description Whether the email is for an existing account. Always true. */ registered?: boolean; + /** @description Warning notifications for the user. */ + userNotifications?: components["schemas"]["GoogleCloudIdentitytoolkitV1UserNotification"][]; }; - /** - * Request message for SignInWithPhoneNumber. - */ + /** @description Request message for SignInWithPhoneNumber. */ GoogleCloudIdentitytoolkitV1SignInWithPhoneNumberRequest: { - /** - * User-entered verification code from an SMS sent to the user's phone. - */ + /** @description User-entered verification code from an SMS sent to the user's phone. */ code?: string; - /** - * A valid ID token for an Identity Platform account. If passed, this request will link the phone number to the user represented by this ID token if the phone number is not in use, or will reauthenticate the user if the phone number is already linked to the user. - */ + /** @description A valid ID token for an Identity Platform account. If passed, this request will link the phone number to the user represented by this ID token if the phone number is not in use, or will reauthenticate the user if the phone number is already linked to the user. */ idToken?: string; + /** @deprecated */ operation?: "VERIFY_OP_UNSPECIFIED" | "SIGN_UP_OR_IN" | "REAUTH" | "UPDATE" | "LINK"; - /** - * The user's phone number to sign in with. This is necessary in the case of uing a temporary proof, in which case it must match the phone number that was authenticated in the request that generated the temporary proof. This field is ignored if a session info is passed. - */ + /** @description The user's phone number to sign in with. This is necessary in the case of uing a temporary proof, in which case it must match the phone number that was authenticated in the request that generated the temporary proof. This field is ignored if a session info is passed. */ phoneNumber?: string; - /** - * Encrypted session information from the response of sendVerificationCode. In the case of authenticating with an SMS code this must be specified, but in the case of using a temporary proof it can be unspecified. - */ + /** @description Encrypted session information from the response of sendVerificationCode. In the case of authenticating with an SMS code this must be specified, but in the case of using a temporary proof it can be unspecified. */ sessionInfo?: string; - /** - * A proof of the phone number verification, provided from a previous signInWithPhoneNumber request. If this is passed, the caller must also pass in the phone_number field the phone number that was verified in the previous request. - */ + /** @description A proof of the phone number verification, provided from a previous signInWithPhoneNumber request. If this is passed, the caller must also pass in the phone_number field the phone number that was verified in the previous request. */ temporaryProof?: string; - /** - * The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. */ tenantId?: string; - /** - * Do not use. - */ + /** @description Do not use. */ verificationProof?: string; }; - /** - * Response message for SignInWithPhoneNumber. - */ + /** @description Response message for SignInWithPhoneNumber. */ GoogleCloudIdentitytoolkitV1SignInWithPhoneNumberResponse: { /** - * The number of seconds until the ID token expires. + * Format: int64 + * @description The number of seconds until the ID token expires. */ expiresIn?: string; - /** - * Identity Platform ID token for the authenticated user. - */ + /** @description Identity Platform ID token for the authenticated user. */ idToken?: string; - /** - * Whether the authenticated user was created by this request. - */ + /** @description Whether the authenticated user was created by this request. */ isNewUser?: boolean; - /** - * The id of the authenticated user. Present in the case of a successful authentication. In the case when the phone could be verified but the account operation could not be performed, a temporary proof will be returned instead. - */ + /** @description The id of the authenticated user. Present in the case of a successful authentication. In the case when the phone could be verified but the account operation could not be performed, a temporary proof will be returned instead. */ localId?: string; - /** - * Phone number of the authenticated user. Always present in the response. - */ + /** @description Phone number of the authenticated user. Always present in the response. */ phoneNumber?: string; - /** - * Refresh token for the authenticated user. - */ + /** @description Refresh token for the authenticated user. */ refreshToken?: string; - /** - * A proof of the phone number verification, provided if a phone authentication is successful but the user operation fails. This happens when the request tries to link a phone number to a user with an ID token or reauthenticate with an ID token but the phone number is linked to a different user. - */ + /** @description A proof of the phone number verification, provided if a phone authentication is successful but the user operation fails. This happens when the request tries to link a phone number to a user with an ID token or reauthenticate with an ID token but the phone number is linked to a different user. */ temporaryProof?: string; /** - * The number of seconds until the temporary proof expires. + * Format: int64 + * @description The number of seconds until the temporary proof expires. */ temporaryProofExpiresIn?: string; - /** - * Do not use. - */ + /** @description Do not use. */ verificationProof?: string; /** - * Do not use. + * Format: int64 + * @description Do not use. */ verificationProofExpiresIn?: string; }; - /** - * Request message for SignUp. - */ + /** @description Request message for SignUp. */ GoogleCloudIdentitytoolkitV1SignUpRequest: { + /** @deprecated */ captchaChallenge?: string; - /** - * The response from a reCaptcha challenge. A reCaptcha response is required when the service detects potential abuse activity. - */ + /** @description The reCAPTCHA token provided by the reCAPTCHA client-side integration. reCAPTCHA Enterprise uses it for assessment. Required when reCAPTCHA enterprise is enabled. */ captchaResponse?: string; - /** - * Whether the user will be disabled upon creation. Disabled accounts are inaccessible except for requests bearing a Google OAuth2 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description The client type: web, Android or iOS. Required when enabling reCAPTCHA enterprise protection. */ + clientType?: + | "CLIENT_TYPE_UNSPECIFIED" + | "CLIENT_TYPE_WEB" + | "CLIENT_TYPE_ANDROID" + | "CLIENT_TYPE_IOS"; + /** @description Whether the user will be disabled upon creation. Disabled accounts are inaccessible except for requests bearing a Google OAuth2 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ disabled?: boolean; - /** - * The display name of the user to be created. - */ + /** @description The display name of the user to be created. */ displayName?: string; - /** - * The email to assign to the created user. The length of the email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. An anonymous user will be created if not provided. - */ + /** @description The email to assign to the created user. The length of the email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec production. An anonymous user will be created if not provided. */ email?: string; - /** - * Whether the user's email is verified. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description Whether the user's email is verified. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ emailVerified?: boolean; - /** - * A valid ID token for an Identity Platform user. If set, this request will link the authentication credential to the user represented by this ID token. For a non-admin request, both the `email` and `password` fields must be set. For an admin request, `local_id` must not be set. - */ + /** @description A valid ID token for an Identity Platform user. If set, this request will link the authentication credential to the user represented by this ID token. For a non-admin request, both the `email` and `password` fields must be set. For an admin request, `local_id` must not be set. */ idToken?: string; + /** @deprecated */ instanceId?: string; - /** - * The ID of the user to create. The ID must be unique within the project that the user is being created under. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description The ID of the user to create. The ID must be unique within the project that the user is being created under. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ localId?: string; - /** - * The multi-factor authentication providers for the user to create. - */ + /** @description The multi-factor authentication providers for the user to create. */ mfaInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaFactor"][]; - /** - * The password to assign to the created user. The password must be be at least 6 characters long. If set, the `email` field must also be set. - */ + /** @description The password to assign to the created user. The password must be be at least 6 characters long. If set, the `email` field must also be set. */ password?: string; - /** - * The phone number of the user to create. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). - */ + /** @description The phone number of the user to create. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ phoneNumber?: string; - /** - * The profile photo url of the user to create. - */ + /** @description The profile photo url of the user to create. */ photoUrl?: string; - /** - * The project ID of the project which the user should belong to. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). If this is not set, the target project is inferred from the scope associated to the Bearer access token. - */ + /** @description The reCAPTCHA version of the reCAPTCHA token in the captcha_response. */ + recaptchaVersion?: "RECAPTCHA_VERSION_UNSPECIFIED" | "RECAPTCHA_ENTERPRISE"; + /** @description The project ID of the project which the user should belong to. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). If this is not set, the target project is inferred from the scope associated to the Bearer access token. */ targetProjectId?: string; - /** - * The ID of the Identity Platform tenant to create a user under. If not set, the user will be created under the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant to create a user under. If not set, the user will be created under the default Identity Platform project. */ tenantId?: string; }; - /** - * Response message for SignUp. - */ + /** @description Response message for SignUp. */ GoogleCloudIdentitytoolkitV1SignUpResponse: { - /** - * The created user's display name. - */ + /** @description The created user's display name. */ displayName?: string; - /** - * The created user's email. - */ + /** @description The created user's email. */ email?: string; /** - * The number of seconds until the ID token expires. + * Format: int64 + * @description The number of seconds until the ID token expires. */ expiresIn?: string; - /** - * An Identity Platform ID token for the created user. This field is only set for non-admin requests. - */ + /** @description An Identity Platform ID token for the created user. This field is only set for non-admin requests. */ idToken?: string; kind?: string; - /** - * The ID of the created user. Always present in the response. - */ + /** @description The ID of the created user. Always present in the response. */ localId?: string; - /** - * An Identity Platform refresh token for the created user. This field is only set for non-admin requests. - */ + /** @description An Identity Platform refresh token for the created user. This field is only set for non-admin requests. */ refreshToken?: string; }; - /** - * Query conditions used to filter results. - */ + /** @description Query conditions used to filter results. */ GoogleCloudIdentitytoolkitV1SqlExpression: { - /** - * A case insensitive string that the account's email should match. Only one of `email`, `phone_number`, or `user_id` should be specified in a SqlExpression. If more than one is specified, only the first (in that order) will be applied. - */ + /** @description A case insensitive string that the account's email should match. Only one of `email`, `phone_number`, or `user_id` should be specified in a SqlExpression. If more than one is specified, only the first (in that order) will be applied. */ email?: string; - /** - * A string that the account's phone number should match. Only one of `email`, `phone_number`, or `user_id` should be specified in a SqlExpression. If more than one is specified, only the first (in that order) will be applied. - */ + /** @description A string that the account's phone number should match. Only one of `email`, `phone_number`, or `user_id` should be specified in a SqlExpression. If more than one is specified, only the first (in that order) will be applied. */ phoneNumber?: string; - /** - * A string that the account's local ID should match. Only one of `email`, `phone_number`, or `user_id` should be specified in a SqlExpression If more than one is specified, only the first (in that order) will be applied. - */ + /** @description A string that the account's local ID should match. Only one of `email`, `phone_number`, or `user_id` should be specified in a SqlExpression If more than one is specified, only the first (in that order) will be applied. */ userId?: string; }; - /** - * Request message for UploadAccount. - */ + /** @description Information about TOTP MFA. */ + GoogleCloudIdentitytoolkitV1TotpInfo: { [key: string]: unknown }; + /** @description Request message for UploadAccount. */ GoogleCloudIdentitytoolkitV1UploadAccountRequest: { - /** - * Whether to overwrite an existing account in Identity Platform with a matching `local_id` in the request. If true, the existing account will be overwritten. If false, an error will be returned. - */ + /** @description Whether to overwrite an existing account in Identity Platform with a matching `local_id` in the request. If true, the existing account will be overwritten. If false, an error will be returned. */ allowOverwrite?: boolean; argon2Parameters?: components["schemas"]["GoogleCloudIdentitytoolkitV1Argon2Parameters"]; /** - * The block size parameter used by the STANDARD_SCRYPT hashing function. This parameter, along with parallelization and cpu_mem_cost help tune the resources needed to hash a password, and should be tuned as processor speeds and memory technologies advance. + * Format: int32 + * @description The block size parameter used by the STANDARD_SCRYPT hashing function. This parameter, along with parallelization and cpu_mem_cost help tune the resources needed to hash a password, and should be tuned as processor speeds and memory technologies advance. */ blockSize?: number; /** - * The CPU memory cost parameter to be used by the STANDARD_SCRYPT hashing function. This parameter, along with block_size and cpu_mem_cost help tune the resources needed to hash a password, and should be tuned as processor speeds and memory technologies advance. + * Format: int32 + * @description The CPU memory cost parameter to be used by the STANDARD_SCRYPT hashing function. This parameter, along with block_size and cpu_mem_cost help tune the resources needed to hash a password, and should be tuned as processor speeds and memory technologies advance. */ cpuMemCost?: number; /** - * If true, the service will do the following list of checks before an account is uploaded: * Duplicate emails * Duplicate federated IDs * Federated ID provider validation If the duplication exists within the list of accounts to be uploaded, it will prevent the entire list from being uploaded. If the email or federated ID is a duplicate of a user already within the project/tenant, the account will not be uploaded, but the rest of the accounts will be unaffected. If false, these checks will be skipped. + * Format: int64 + * @deprecated */ delegatedProjectNumber?: string; /** - * The desired key length for the STANDARD_SCRYPT hashing function. Must be at least 1. + * Format: int32 + * @description The desired key length for the STANDARD_SCRYPT hashing function. Must be at least 1. */ dkLen?: number; - /** - * Required. The hashing function used to hash the account passwords. Must be one of the following: * HMAC_SHA256 * HMAC_SHA1 * HMAC_MD5 * SCRYPT * PBKDF_SHA1 * MD5 * HMAC_SHA512 * SHA1 * BCRYPT * PBKDF2_SHA256 * SHA256 * SHA512 * STANDARD_SCRYPT * ARGON2 - */ + /** @description Required. The hashing function used to hash the account passwords. Must be one of the following: * HMAC_SHA256 * HMAC_SHA1 * HMAC_MD5 * SCRYPT * PBKDF_SHA1 * MD5 * HMAC_SHA512 * SHA1 * BCRYPT * PBKDF2_SHA256 * SHA256 * SHA512 * STANDARD_SCRYPT * ARGON2 */ hashAlgorithm?: string; /** - * Memory cost for hash calculation. Only required when the hashing function is SCRYPT. + * Format: int32 + * @description Memory cost for hash calculation. Only required when the hashing function is SCRYPT. */ memoryCost?: number; /** - * The parallelization cost parameter to be used by the STANDARD_SCRYPT hashing function. This parameter, along with block_size and cpu_mem_cost help tune the resources needed to hash a password, and should be tuned as processor speeds and memory technologies advance. + * Format: int32 + * @description The parallelization cost parameter to be used by the STANDARD_SCRYPT hashing function. This parameter, along with block_size and cpu_mem_cost help tune the resources needed to hash a password, and should be tuned as processor speeds and memory technologies advance. */ parallelization?: number; - /** - * Password and salt order when verify password. - */ + /** @description Password and salt order when verify password. */ passwordHashOrder?: "UNSPECIFIED_ORDER" | "SALT_AND_PASSWORD" | "PASSWORD_AND_SALT"; /** - * The number of rounds used for hash calculation. Only required for the following hashing functions: * MD5 * SHA1 * SHA256 * SHA512 * PBKDF_SHA1 * PBKDF2_SHA256 * SCRYPT + * Format: int32 + * @description The number of rounds used for hash calculation. Only required for the following hashing functions: * MD5 * SHA1 * SHA256 * SHA512 * PBKDF_SHA1 * PBKDF2_SHA256 * SCRYPT */ rounds?: number; /** - * One or more bytes to be inserted between the salt and plain text password. For stronger security, this should be a single non-printable character. + * Format: byte + * @description One or more bytes to be inserted between the salt and plain text password. For stronger security, this should be a single non-printable character. */ saltSeparator?: string; + /** @description If true, the service will do the following list of checks before an account is uploaded: * Duplicate emails * Duplicate federated IDs * Federated ID provider validation If the duplication exists within the list of accounts to be uploaded, it will prevent the entire list from being uploaded. If the email or federated ID is a duplicate of a user already within the project/tenant, the account will not be uploaded, but the rest of the accounts will be unaffected. If false, these checks will be skipped. */ sanityCheck?: boolean; /** - * The signer key used to hash the password. Required for the following hashing functions: * SCRYPT, * HMAC_MD5, * HMAC_SHA1, * HMAC_SHA256, * HMAC_SHA512 + * Format: byte + * @description The signer key used to hash the password. Required for the following hashing functions: * SCRYPT, * HMAC_MD5, * HMAC_SHA1, * HMAC_SHA256, * HMAC_SHA512 */ signerKey?: string; - /** - * The ID of the Identity Platform tenant the account belongs to. - */ + /** @description The ID of the Identity Platform tenant the account belongs to. */ tenantId?: string; - /** - * A list of accounts to upload. - */ + /** @description A list of accounts to upload. `local_id` is required for each user; everything else is optional. */ users?: components["schemas"]["GoogleCloudIdentitytoolkitV1UserInfo"][]; }; - /** - * Response message for UploadAccount. - */ + /** @description Response message for UploadAccount. */ GoogleCloudIdentitytoolkitV1UploadAccountResponse: { - /** - * Detailed error info for accounts that cannot be uploaded. - */ + /** @description Detailed error info for accounts that cannot be uploaded. */ error?: components["schemas"]["GoogleCloudIdentitytoolkitV1ErrorInfo"][]; + /** @deprecated */ kind?: string; }; - /** - * An Identity Platform account's information. - */ + /** @description An Identity Platform account's information. */ GoogleCloudIdentitytoolkitV1UserInfo: { /** - * The time, in milliseconds from epoch, when the account was created. + * Format: int64 + * @description The time, in milliseconds from epoch, when the account was created. */ createdAt?: string; - /** - * Custom claims to be added to any ID tokens minted for the account. Should be at most 1,000 characters in length and in valid JSON format. - */ + /** @description Custom claims to be added to any ID tokens minted for the account. Should be at most 1,000 characters in length and in valid JSON format. */ customAttributes?: string; - /** - * Output only. Whether this account has been authenticated using SignInWithCustomToken. - */ + /** @description Output only. Whether this account has been authenticated using SignInWithCustomToken. */ customAuth?: boolean; - /** - * Output only. The date of birth set for the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. - */ + /** @description Output only. The date of birth set for the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. */ dateOfBirth?: string; - /** - * Whether the account is disabled. Disabled accounts are inaccessible except for requests bearing a Google OAuth2 credential with proper permissions. - */ + /** @description Whether the account is disabled. Disabled accounts are inaccessible except for requests bearing a Google OAuth2 credential with proper permissions. */ disabled?: boolean; - /** - * The display name of the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. - */ + /** @description The display name of the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. */ displayName?: string; - /** - * The account's email address. The length of the email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec. - */ + /** @description The account's email address. The length of the email should be less than 256 characters and in the format of `name@domain.tld`. The email should also match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec. */ email?: string; - /** - * Output only. Whether the account can authenticate with email link. - */ + /** @description Output only. Whether the account can authenticate with email link. */ emailLinkSignin?: boolean; - /** - * Whether the account's email address has been verified. - */ + /** @description Whether the account's email address has been verified. */ emailVerified?: boolean; - /** - * The first email address associated with this account. The account's initial email cannot be changed once set and is used to recover access to this account if lost via the RECOVER_EMAIL flow in GetOobCode. Should match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec. - */ + /** @description The first email address associated with this account. The account's initial email cannot be changed once set and is used to recover access to this account if lost via the RECOVER_EMAIL flow in GetOobCode. Should match the [RFC 822](https://tools.ietf.org/html/rfc822) addr-spec. */ initialEmail?: string; - /** - * Output only. The language preference of the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. - */ + /** @description Output only. The language preference of the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. */ language?: string; /** - * The last time, in milliseconds from epoch, this account was logged into. + * Format: int64 + * @description The last time, in milliseconds from epoch, this account was logged into. */ lastLoginAt?: string; /** - * Timestamp when an ID token was last minted for this account. + * Format: google-datetime + * @description Timestamp when an ID token was last minted for this account. */ lastRefreshAt?: string; - /** - * Immutable. The unique ID of the account. - */ + /** @description Immutable. The unique ID of the account. */ localId?: string; - /** - * Information on which multi-factor authentication providers are enabled for this account. - */ + /** @description Information on which multi-factor authentication providers are enabled for this account. */ mfaInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1MfaEnrollment"][]; /** - * The account's hashed password. Only accessible by requests bearing a Google OAuth2 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). + * Format: byte + * @description The account's hashed password. Only accessible by requests bearing a Google OAuth2 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ passwordHash?: string; /** - * The timestamp, in milliseconds from the epoch of 1970-01-01T00:00:00Z, when the account's password was last updated. + * Format: double + * @description The timestamp, in milliseconds from the epoch of 1970-01-01T00:00:00Z, when the account's password was last updated. */ passwordUpdatedAt?: number; - /** - * The account's phone number. - */ + /** @description The account's phone number. */ phoneNumber?: string; - /** - * The URL of the account's profile photo. This account attribute is not used by Identity Platform. It is available for informational purposes only. - */ + /** @description The URL of the account's profile photo. This account attribute is not used by Identity Platform. It is available for informational purposes only. */ photoUrl?: string; - /** - * Information about the user as provided by various Identity Providers. - */ + /** @description Information about the user as provided by various Identity Providers. */ providerUserInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV1ProviderUserInfo"][]; - /** - * Input only. Plain text password used to update a account's password. This field is only ever used as input in a request. Identity Platform uses cryptographically secure hashing when managing passwords and will never store or transmit a user's password in plain text. - */ + /** @description Input only. Plain text password used to update a account's password. This field is only ever used as input in a request. Identity Platform uses cryptographically secure hashing when managing passwords and will never store or transmit a user's password in plain text. */ rawPassword?: string; /** - * The account's password salt. Only accessible by requests bearing a Google OAuth2 credential with proper permissions. + * Format: byte + * @description The account's password salt. Only accessible by requests bearing a Google OAuth2 credential with proper permissions. */ salt?: string; - /** - * Output only. This account's screen name at Twitter or login name at GitHub. - */ + /** @description Output only. This account's screen name at Twitter or login name at GitHub. */ screenName?: string; - /** - * ID of the tenant this account belongs to. Only set if this account belongs to a tenant. - */ + /** @description ID of the tenant this account belongs to. Only set if this account belongs to a tenant. */ tenantId?: string; - /** - * Output only. The time zone preference of the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. - */ + /** @description Output only. The time zone preference of the account. This account attribute is not used by Identity Platform. It is available for informational purposes only. */ timeZone?: string; /** - * Oldest timestamp, in seconds since epoch, that an ID token should be considered valid. All ID tokens issued before this time are considered invalid. + * Format: int64 + * @description Oldest timestamp, in seconds since epoch, that an ID token should be considered valid. All ID tokens issued before this time are considered invalid. */ validSince?: string; /** - * The version of the account's password. Only accessible by requests bearing a Google OAuth2 credential with proper permissions. + * Format: int32 + * @description The version of the account's password. Only accessible by requests bearing a Google OAuth2 credential with proper permissions. */ version?: number; }; - /** - * Request message for VerifyIosClient - */ + /** @description Warning notifications for the user. */ + GoogleCloudIdentitytoolkitV1UserNotification: { + /** @description Warning notification enum. Can be used for localization. */ + notificationCode?: + | "NOTIFICATION_CODE_UNSPECIFIED" + | "MISSING_LOWERCASE_CHARACTER" + | "MISSING_UPPERCASE_CHARACTER" + | "MISSING_NUMERIC_CHARACTER" + | "MISSING_NON_ALPHANUMERIC_CHARACTER" + | "MINIMUM_PASSWORD_LENGTH" + | "MAXIMUM_PASSWORD_LENGTH"; + /** @description Warning notification string. Can be used as fallback. */ + notificationMessage?: string; + }; + /** @description Request message for VerifyIosClient */ GoogleCloudIdentitytoolkitV1VerifyIosClientRequest: { - /** - * A device token that the iOS client gets after registering to APNs (Apple Push Notification service). - */ + /** @description A device token that the iOS client gets after registering to APNs (Apple Push Notification service). */ appToken?: string; - /** - * Whether the app token is in the iOS sandbox. If false, the app token is in the production environment. - */ + /** @description Whether the app token is in the iOS sandbox. If false, the app token is in the production environment. */ isSandbox?: boolean; }; - /** - * Response message for VerifyIosClient. - */ + /** @description Response message for VerifyIosClient. */ GoogleCloudIdentitytoolkitV1VerifyIosClientResponse: { - /** - * Receipt of successful app token validation. - */ + /** @description Receipt of successful app token validation. */ receipt?: string; /** - * Suggested time that the client should wait in seconds for delivery of the push notification. + * Format: int64 + * @description Suggested time that the client should wait in seconds for delivery of the push notification. */ suggestedTimeout?: string; }; - /** - * Configuration options related to authenticating an anonymous user. - */ + /** @description Defines a policy of allowing every region by default and adding disallowed regions to a disallow list. */ + GoogleCloudIdentitytoolkitAdminV2AllowByDefault: { + /** @description Two letter unicode region codes to disallow as defined by https://cldr.unicode.org/ The full list of these region codes is here: https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json */ + disallowedRegions?: string[]; + }; + /** @description Defines a policy of only allowing regions by explicitly adding them to an allowlist. */ + GoogleCloudIdentitytoolkitAdminV2AllowlistOnly: { + /** @description Two letter unicode region codes to allow as defined by https://cldr.unicode.org/ The full list of these region codes is here: https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json */ + allowedRegions?: string[]; + }; + /** @description Configuration options related to authenticating an anonymous user. */ GoogleCloudIdentitytoolkitAdminV2Anonymous: { - /** - * Whether anonymous user auth is enabled for the project or not. - */ + /** @description Whether anonymous user auth is enabled for the project or not. */ enabled?: boolean; }; - /** - * Additional config for SignInWithApple. - */ + /** @description Additional config for SignInWithApple. */ GoogleCloudIdentitytoolkitAdminV2AppleSignInConfig: { - /** - * A list of Bundle ID's usable by this project - */ + /** @description A list of Bundle ID's usable by this project */ bundleIds?: string[]; codeFlowConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2CodeFlowConfig"]; }; - /** - * Configuration related to Blocking Functions. - */ + /** @description Configuration related to Blocking Functions. */ GoogleCloudIdentitytoolkitAdminV2BlockingFunctionsConfig: { forwardInboundCredentials?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ForwardInboundCredentials"]; - /** - * Map of Trigger to event type. Key should be one of the supported event types: "beforeCreate", "beforeSignIn" - */ + /** @description Map of Trigger to event type. Key should be one of the supported event types: "beforeCreate", "beforeSignIn" */ triggers?: { [key: string]: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Trigger"]; }; }; - /** - * Options related to how clients making requests on behalf of a project should be configured. - */ + /** @description Options related to how clients making requests on behalf of a project should be configured. */ GoogleCloudIdentitytoolkitAdminV2ClientConfig: { - /** - * Output only. API key that can be used when making requests for this project. - */ + /** @description Output only. API key that can be used when making requests for this project. */ apiKey?: string; - /** - * Output only. Firebase subdomain. - */ + /** @description Output only. Firebase subdomain. */ firebaseSubdomain?: string; permissions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Permissions"]; }; - /** - * Additional config for Apple for code flow. - */ + /** @description Options related to how clients making requests on behalf of a tenant should be configured. */ + GoogleCloudIdentitytoolkitAdminV2ClientPermissionConfig: { + permissions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ClientPermissions"]; + }; + /** @description Configuration related to restricting a user's ability to affect their account. */ + GoogleCloudIdentitytoolkitAdminV2ClientPermissions: { + /** @description When true, end users cannot delete their account on the associated project through any of our API methods */ + disabledUserDeletion?: boolean; + /** @description When true, end users cannot sign up for a new account on the associated project through any of our API methods */ + disabledUserSignup?: boolean; + }; + /** @description Additional config for Apple for code flow. */ GoogleCloudIdentitytoolkitAdminV2CodeFlowConfig: { - /** - * Key ID for the private key. - */ + /** @description Key ID for the private key. */ keyId?: string; - /** - * Private key used for signing the client secret JWT. - */ + /** @description Private key used for signing the client secret JWT. */ privateKey?: string; - /** - * Apple Developer Team ID. - */ + /** @description Apple Developer Team ID. */ teamId?: string; }; - /** - * Represents an Identity Toolkit project. - */ + /** @description Represents an Identity Toolkit project. */ GoogleCloudIdentitytoolkitAdminV2Config: { - /** - * List of domains authorized for OAuth redirects - */ + /** @description List of domains authorized for OAuth redirects */ authorizedDomains?: string[]; + /** @description Whether anonymous users will be auto-deleted after a period of 30 days. */ + autodeleteAnonymousUsers?: boolean; blockingFunctions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2BlockingFunctionsConfig"]; client?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ClientConfig"]; + emailPrivacyConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig"]; mfa?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig"]; monitoring?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MonitoringConfig"]; multiTenant?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MultiTenantConfig"]; - /** - * Output only. The name of the Config resource. Example: "projects/my-awesome-project/config" - */ + /** @description Output only. The name of the Config resource. Example: "projects/my-awesome-project/config" */ name?: string; notification?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2NotificationConfig"]; + passwordPolicyConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2PasswordPolicyConfig"]; quota?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2QuotaConfig"]; + recaptchaConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2RecaptchaConfig"]; signIn?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SignInConfig"]; - /** - * Output only. The subtype of this config. - */ + smsRegionConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig"]; + /** @description Output only. The subtype of this config. */ subtype?: "SUBTYPE_UNSPECIFIED" | "IDENTITY_PLATFORM" | "FIREBASE_AUTH"; }; - /** - * Standard Identity Toolkit-trusted IDPs. - */ - GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdp: { + /** @description Custom strength options to enforce on user passwords. */ + GoogleCloudIdentitytoolkitAdminV2CustomStrengthOptions: { + /** @description The password must contain a lower case character. */ + containsLowercaseCharacter?: boolean; + /** @description The password must contain a non alpha numeric character. */ + containsNonAlphanumericCharacter?: boolean; + /** @description The password must contain a number. */ + containsNumericCharacter?: boolean; + /** @description The password must contain an upper case character. */ + containsUppercaseCharacter?: boolean; /** - * Description of the Idp + * Format: int32 + * @description Maximum password length. No default max length */ - description?: string; + maxPasswordLength?: number; /** - * Id the of Idp + * Format: int32 + * @description Minimum password length. Range from 6 to 30 */ + minPasswordLength?: number; + }; + /** @description Standard Identity Toolkit-trusted IDPs. */ + GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdp: { + /** @description Description of the Idp */ + description?: string; + /** @description Id the of Idp */ idpId?: string; }; - /** - * Configurations options for authenticating with a the standard set of Identity Toolkit-trusted IDPs. - */ + /** @description Configurations options for authenticating with a the standard set of Identity Toolkit-trusted IDPs. */ GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig: { appleSignInConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2AppleSignInConfig"]; - /** - * OAuth client ID. - */ + /** @description OAuth client ID. */ clientId?: string; - /** - * OAuth client secret. - */ + /** @description OAuth client secret. */ clientSecret?: string; - /** - * True if allows the user to sign in with the provider. - */ + /** @description True if allows the user to sign in with the provider. */ enabled?: boolean; - /** - * The name of the DefaultSupportedIdpConfig resource, for example: "projects/my-awesome-project/defaultSupportedIdpConfigs/google.com" - */ + /** @description The name of the DefaultSupportedIdpConfig resource, for example: "projects/my-awesome-project/defaultSupportedIdpConfigs/google.com" */ name?: string; }; - /** - * Information of custom domain DNS verification. By default, default_domain will be used. A custom domain can be configured using VerifyCustomDomain. - */ + /** @description Information of custom domain DNS verification. By default, default_domain will be used. A custom domain can be configured using VerifyCustomDomain. */ GoogleCloudIdentitytoolkitAdminV2DnsInfo: { - /** - * Output only. The applied verified custom domain. - */ + /** @description Output only. The applied verified custom domain. */ customDomain?: string; - /** - * Output only. The current verification state of the custom domain. The custom domain will only be used once the domain verification is successful. - */ + /** @description Output only. The current verification state of the custom domain. The custom domain will only be used once the domain verification is successful. */ customDomainState?: | "VERIFICATION_STATE_UNSPECIFIED" | "NOT_STARTED" @@ -1991,88 +3351,56 @@ export interface components { | "FAILED" | "SUCCEEDED"; /** - * Output only. The timestamp of initial request for the current domain verification. + * Format: google-datetime + * @description Output only. The timestamp of initial request for the current domain verification. */ domainVerificationRequestTime?: string; - /** - * Output only. The custom domain that's to be verified. - */ + /** @description Output only. The custom domain that's to be verified. */ pendingCustomDomain?: string; - /** - * Whether to use custom domain. - */ + /** @description Whether to use custom domain. */ useCustomDomain?: boolean; }; - /** - * Configuration options related to authenticating a user by their email address. - */ + /** @description Configuration options related to authenticating a user by their email address. */ GoogleCloudIdentitytoolkitAdminV2Email: { - /** - * Whether email auth is enabled for the project or not. - */ + /** @description Whether email auth is enabled for the project or not. */ enabled?: boolean; - /** - * Whether a password is required for email auth or not. If true, both an email and password must be provided to sign in. If false, a user may sign in via either email/password or email link. - */ + /** @description Whether a password is required for email auth or not. If true, both an email and password must be provided to sign in. If false, a user may sign in via either email/password or email link. */ passwordRequired?: boolean; }; - /** - * Email template. The subject and body fields can contain the following placeholders which will be replaced with the appropriate values: %LINK% - The link to use to redeem the send OOB code. %EMAIL% - The email where the email is being sent. %NEW_EMAIL% - The new email being set for the account (when applicable). %APP_NAME% - The GCP project's display name. %DISPLAY_NAME% - The user's display name. - */ + /** @description Configuration for settings related to email privacy and public visibility. Settings in this config protect against email enumeration, but may make some trade-offs in user-friendliness. */ + GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig: { + /** @description Migrates the project to a state of improved email privacy. For example certain error codes are more generic to avoid giving away information on whether the account exists. In addition, this disables certain features that as a side-effect allow user enumeration. Enabling this toggle disables the fetchSignInMethodsForEmail functionality and changing the user's email to an unverified email. It is recommended to remove dependence on this functionality and enable this toggle to improve user privacy. */ + enableImprovedEmailPrivacy?: boolean; + }; + /** @description Email template. The subject and body fields can contain the following placeholders which will be replaced with the appropriate values: %LINK% - The link to use to redeem the send OOB code. %EMAIL% - The email where the email is being sent. %NEW_EMAIL% - The new email being set for the account (when applicable). %APP_NAME% - The GCP project's display name. %DISPLAY_NAME% - The user's display name. */ GoogleCloudIdentitytoolkitAdminV2EmailTemplate: { - /** - * Email body - */ + /** @description Email body */ body?: string; - /** - * Email body format - */ + /** @description Email body format */ bodyFormat?: "BODY_FORMAT_UNSPECIFIED" | "PLAIN_TEXT" | "HTML"; - /** - * Output only. Whether the body or subject of the email is customized. - */ + /** @description Output only. Whether the body or subject of the email is customized. */ customized?: boolean; - /** - * Reply-to address - */ + /** @description Reply-to address */ replyTo?: string; - /** - * Sender display name - */ + /** @description Sender display name */ senderDisplayName?: string; - /** - * Local part of From address - */ + /** @description Local part of From address */ senderLocalPart?: string; - /** - * Subject of the email - */ + /** @description Subject of the email */ subject?: string; }; - /** - * Indicates which credentials to pass to the registered Blocking Functions. - */ + /** @description Indicates which credentials to pass to the registered Blocking Functions. */ GoogleCloudIdentitytoolkitAdminV2ForwardInboundCredentials: { - /** - * Whether to pass the user's OAuth identity provider's access token. - */ + /** @description Whether to pass the user's OAuth identity provider's access token. */ accessToken?: boolean; - /** - * Whether to pass the user's OIDC identity provider's ID token. - */ + /** @description Whether to pass the user's OIDC identity provider's ID token. */ idToken?: boolean; - /** - * Whether to pass the user's OAuth identity provider's refresh token. - */ + /** @description Whether to pass the user's OAuth identity provider's refresh token. */ refreshToken?: boolean; }; - /** - * History information of the hash algorithm and key. Different accounts' passwords may be generated by different version. - */ + /** @description History information of the hash algorithm and key. Different accounts' passwords may be generated by different version. */ GoogleCloudIdentitytoolkitAdminV2HashConfig: { - /** - * Output only. Different password hash algorithms used in Identity Toolkit. - */ + /** @description Output only. Different password hash algorithms used in Identity Toolkit. */ algorithm?: | "HASH_ALGORITHM_UNSPECIFIED" | "HMAC_SHA256" @@ -2089,860 +3417,4464 @@ export interface components { | "SHA512" | "STANDARD_SCRYPT"; /** - * Output only. Memory cost for hash calculation. Used by scrypt and other similar password derivation algorithms. See https://tools.ietf.org/html/rfc7914 for explanation of field. + * Format: int32 + * @description Output only. Memory cost for hash calculation. Used by scrypt and other similar password derivation algorithms. See https://tools.ietf.org/html/rfc7914 for explanation of field. */ memoryCost?: number; /** - * Output only. How many rounds for hash calculation. Used by scrypt and other similar password derivation algorithms. + * Format: int32 + * @description Output only. How many rounds for hash calculation. Used by scrypt and other similar password derivation algorithms. */ rounds?: number; - /** - * Output only. Non-printable character to be inserted between the salt and plain text password in base64. - */ + /** @description Output only. Non-printable character to be inserted between the salt and plain text password in base64. */ saltSeparator?: string; - /** - * Output only. Signer key in base64. - */ + /** @description Output only. Signer key in base64. */ signerKey?: string; }; - /** - * The IDP's certificate data to verify the signature in the SAMLResponse issued by the IDP. - */ + /** @description The IDP's certificate data to verify the signature in the SAMLResponse issued by the IDP. */ GoogleCloudIdentitytoolkitAdminV2IdpCertificate: { - /** - * The x509 certificate - */ + /** @description The x509 certificate */ x509Certificate?: string; }; - /** - * The SAML IdP (Identity Provider) configuration when the project acts as the relying party. - */ + /** @description The SAML IdP (Identity Provider) configuration when the project acts as the relying party. */ GoogleCloudIdentitytoolkitAdminV2IdpConfig: { - /** - * IDP's public keys for verifying signature in the assertions. - */ + /** @description IDP's public keys for verifying signature in the assertions. */ idpCertificates?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2IdpCertificate"][]; - /** - * Unique identifier for all SAML entities. - */ + /** @description Unique identifier for all SAML entities. */ idpEntityId?: string; - /** - * Indicates if outbounding SAMLRequest should be signed. - */ + /** @description Indicates if outbounding SAMLRequest should be signed. */ signRequest?: boolean; - /** - * URL to send Authentication request to. - */ + /** @description URL to send Authentication request to. */ ssoUrl?: string; }; - /** - * A pair of SAML RP-IDP configurations when the project acts as the relying party. - */ + /** @description A pair of SAML RP-IDP configurations when the project acts as the relying party. */ GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig: { - /** - * The config's display name set by developers. - */ + /** @description The config's display name set by developers. */ displayName?: string; - /** - * True if allows the user to sign in with the provider. - */ + /** @description True if allows the user to sign in with the provider. */ enabled?: boolean; idpConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2IdpConfig"]; - /** - * The name of the InboundSamlConfig resource, for example: 'projects/my-awesome-project/inboundSamlConfigs/my-config-id'. Ignored during create requests. - */ + /** @description The name of the InboundSamlConfig resource, for example: 'projects/my-awesome-project/inboundSamlConfigs/my-config-id'. Ignored during create requests. */ name?: string; spConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SpConfig"]; }; - /** - * Settings that the tenants will inherit from project level. - */ + /** @description Settings that the tenants will inherit from project level. */ GoogleCloudIdentitytoolkitAdminV2Inheritance: { - /** - * Whether to allow the tenant to inherit custom domains, email templates, and custom SMTP settings. If true, email sent from tenant will follow the project level email sending configurations. If false (by default), emails will go with the default settings with no customizations. - */ + /** @description Whether to allow the tenant to inherit custom domains, email templates, and custom SMTP settings. If true, email sent from tenant will follow the project level email sending configurations. If false (by default), emails will go with the default settings with no customizations. */ emailSendingConfig?: boolean; }; - /** - * Response for DefaultSupportedIdpConfigs - */ + /** @description Request for InitializeIdentityPlatform. */ + GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformRequest: { [key: string]: unknown }; + /** @description Response for InitializeIdentityPlatform. Empty for now. */ + GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformResponse: { [key: string]: unknown }; + /** @description Response for DefaultSupportedIdpConfigs */ GoogleCloudIdentitytoolkitAdminV2ListDefaultSupportedIdpConfigsResponse: { - /** - * The set of configs. - */ + /** @description The set of configs. */ defaultSupportedIdpConfigs?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"][]; - /** - * Token to retrieve the next page of results, or empty if there are no more results in the list. - */ + /** @description Token to retrieve the next page of results, or empty if there are no more results in the list. */ nextPageToken?: string; }; - /** - * Response for ListDefaultSupportedIdps - */ + /** @description Response for ListDefaultSupportedIdps */ GoogleCloudIdentitytoolkitAdminV2ListDefaultSupportedIdpsResponse: { - /** - * The set of configs. - */ + /** @description The set of configs. */ defaultSupportedIdps?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdp"][]; - /** - * Token to retrieve the next page of results, or empty if there are no more results in the list. - */ + /** @description Token to retrieve the next page of results, or empty if there are no more results in the list. */ nextPageToken?: string; }; - /** - * Response for ListInboundSamlConfigs - */ + /** @description Response for ListInboundSamlConfigs */ GoogleCloudIdentitytoolkitAdminV2ListInboundSamlConfigsResponse: { - /** - * The set of configs. - */ + /** @description The set of configs. */ inboundSamlConfigs?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"][]; - /** - * Token to retrieve the next page of results, or empty if there are no more results in the list. - */ + /** @description Token to retrieve the next page of results, or empty if there are no more results in the list. */ nextPageToken?: string; }; - /** - * Response for ListOAuthIdpConfigs - */ + /** @description Response for ListOAuthIdpConfigs */ GoogleCloudIdentitytoolkitAdminV2ListOAuthIdpConfigsResponse: { - /** - * Token to retrieve the next page of results, or empty if there are no more results in the list. - */ + /** @description Token to retrieve the next page of results, or empty if there are no more results in the list. */ nextPageToken?: string; - /** - * The set of configs. - */ + /** @description The set of configs. */ oauthIdpConfigs?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"][]; }; - /** - * Response message for ListTenants. - */ + /** @description Response message for ListTenants. */ GoogleCloudIdentitytoolkitAdminV2ListTenantsResponse: { - /** - * The token to get the next page of results. - */ + /** @description The token to get the next page of results. */ nextPageToken?: string; - /** - * A list of tenants under the given agent project. - */ + /** @description A list of tenants under the given agent project. */ tenants?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Tenant"][]; }; - /** - * Configuration related to monitoring project activity. - */ + /** @description Configuration related to monitoring project activity. */ GoogleCloudIdentitytoolkitAdminV2MonitoringConfig: { requestLogging?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2RequestLogging"]; }; - /** - * Options related to MultiFactor Authentication for the project. - */ + /** @description Options related to MultiFactor Authentication for the project. */ GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig: { - /** - * A list of usable second factors for this project. - */ + /** @description A list of usable second factors for this project. */ enabledProviders?: ("PROVIDER_UNSPECIFIED" | "PHONE_SMS")[]; - /** - * Whether MultiFactor Authentication has been enabled for this project. - */ + /** @description A list of usable second factors for this project along with their configurations. This field does not support phone based MFA, for that use the 'enabled_providers' field. */ + providerConfigs?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ProviderConfig"][]; + /** @description Whether MultiFactor Authentication has been enabled for this project. */ state?: "STATE_UNSPECIFIED" | "DISABLED" | "ENABLED" | "MANDATORY"; }; - /** - * Configuration related to multi-tenant functionality. - */ + /** @description Configuration related to multi-tenant functionality. */ GoogleCloudIdentitytoolkitAdminV2MultiTenantConfig: { - /** - * Whether this project can have tenants or not. - */ + /** @description Whether this project can have tenants or not. */ allowTenants?: boolean; - /** - * The default cloud parent org or folder that the tenant project should be created under. The parent resource name should be in the format of "/", such as "folders/123" or "organizations/456". If the value is not set, the tenant will be created under the same organization or folder as the agent project. - */ + /** @description The default cloud parent org or folder that the tenant project should be created under. The parent resource name should be in the format of "/", such as "folders/123" or "organizations/456". If the value is not set, the tenant will be created under the same organization or folder as the agent project. */ defaultTenantLocation?: string; }; - /** - * Configuration related to sending notifications to users. - */ + /** @description Configuration related to sending notifications to users. */ GoogleCloudIdentitytoolkitAdminV2NotificationConfig: { - /** - * Default locale used for email and SMS in IETF BCP 47 format. - */ + /** @description Default locale used for email and SMS in IETF BCP 47 format. */ defaultLocale?: string; sendEmail?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SendEmail"]; sendSms?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SendSms"]; }; - /** - * Configuration options for authenticating with an OAuth IDP. - */ + /** @description Configuration options for authenticating with an OAuth IDP. */ GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig: { - /** - * The client id of an OAuth client. - */ + /** @description The client id of an OAuth client. */ clientId?: string; - /** - * The client secret of the OAuth client, to enable OIDC code flow. - */ + /** @description The client secret of the OAuth client, to enable OIDC code flow. */ clientSecret?: string; - /** - * The config's display name set by developers. - */ + /** @description The config's display name set by developers. */ displayName?: string; - /** - * True if allows the user to sign in with the provider. - */ + /** @description True if allows the user to sign in with the provider. */ enabled?: boolean; - /** - * For OIDC Idps, the issuer identifier. - */ + /** @description For OIDC Idps, the issuer identifier. */ issuer?: string; - /** - * The name of the OAuthIdpConfig resource, for example: 'projects/my-awesome-project/oauthIdpConfigs/oauth-config-id'. Ignored during create requests. - */ + /** @description The name of the OAuthIdpConfig resource, for example: 'projects/my-awesome-project/oauthIdpConfigs/oauth-config-id'. Ignored during create requests. */ name?: string; responseType?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthResponseType"]; }; - /** - * The response type to request for in the OAuth authorization flow. You can set either `id_token` or `code` to true, but not both. Setting both types to be simultaneously true (`{code: true, id_token: true}`) is not yet supported. See https://openid.net/specs/openid-connect-core-1_0.html#Authentication for a mapping of response type to OAuth 2.0 flow. - */ + /** @description The response type to request for in the OAuth authorization flow. You can set either `id_token` or `code` to true, but not both. Setting both types to be simultaneously true (`{code: true, id_token: true}`) is not yet supported. See https://openid.net/specs/openid-connect-core-1_0.html#Authentication for a mapping of response type to OAuth 2.0 flow. */ GoogleCloudIdentitytoolkitAdminV2OAuthResponseType: { - /** - * If true, authorization code is returned from IdP's authorization endpoint. - */ + /** @description If true, authorization code is returned from IdP's authorization endpoint. */ code?: boolean; - /** - * If true, ID token is returned from IdP's authorization endpoint. - */ + /** @description If true, ID token is returned from IdP's authorization endpoint. */ idToken?: boolean; /** - * Do not use. The `token` response type is not supported at the moment. + * @deprecated + * @description Do not use. The `token` response type is not supported at the moment. */ token?: boolean; }; - /** - * Configuration related to restricting a user's ability to affect their account. - */ - GoogleCloudIdentitytoolkitAdminV2Permissions: { + /** @description The configuration for the password policy on the project. */ + GoogleCloudIdentitytoolkitAdminV2PasswordPolicyConfig: { + /** @description Users must have a password compliant with the password policy to sign-in. */ + forceUpgradeOnSignin?: boolean; /** - * When true, end users cannot delete their account on the associated project through any of our API methods + * Format: google-datetime + * @description Output only. The last time the password policy on the project was updated. */ - disabledUserDeletion?: boolean; + lastUpdateTime?: string; + /** @description Which enforcement mode to use for the password policy. */ + passwordPolicyEnforcementState?: + | "PASSWORD_POLICY_ENFORCEMENT_STATE_UNSPECIFIED" + | "OFF" + | "ENFORCE"; + /** @description Must be of length 1. Contains the strength attributes for the password policy. */ + passwordPolicyVersions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2PasswordPolicyVersion"][]; + }; + /** @description The strength attributes for the password policy on the project. */ + GoogleCloudIdentitytoolkitAdminV2PasswordPolicyVersion: { + customStrengthOptions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2CustomStrengthOptions"]; /** - * When true, end users cannot sign up for a new account on the associated project through any of our API methods + * Format: int32 + * @description Output only. schema version number for the password policy */ + schemaVersion?: number; + }; + /** @description Configuration related to restricting a user's ability to affect their account. */ + GoogleCloudIdentitytoolkitAdminV2Permissions: { + /** @description When true, end users cannot delete their account on the associated project through any of our API methods */ + disabledUserDeletion?: boolean; + /** @description When true, end users cannot sign up for a new account on the associated project through any of our API methods */ disabledUserSignup?: boolean; }; - /** - * Configuration options related to authenticated a user by their phone number. - */ + /** @description Configuration options related to authenticated a user by their phone number. */ GoogleCloudIdentitytoolkitAdminV2PhoneNumber: { - /** - * Whether phone number auth is enabled for the project or not. - */ + /** @description Whether phone number auth is enabled for the project or not. */ enabled?: boolean; - /** - * A map of that can be used for phone auth testing. - */ + /** @description A map of that can be used for phone auth testing. */ testPhoneNumbers?: { [key: string]: string }; }; - /** - * Configuration related to quotas. - */ + /** @description ProviderConfig describes the supported MFA providers along with their configurations. */ + GoogleCloudIdentitytoolkitAdminV2ProviderConfig: { + /** @description Describes the state of the MultiFactor Authentication type. */ + state?: "MFA_STATE_UNSPECIFIED" | "DISABLED" | "ENABLED" | "MANDATORY"; + totpProviderConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2TotpMfaProviderConfig"]; + }; + /** @description Configuration related to quotas. */ GoogleCloudIdentitytoolkitAdminV2QuotaConfig: { signUpQuotaConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2TemporaryQuota"]; }; - /** - * Configuration for logging requests made to this project to Stackdriver Logging - */ + /** @description The reCAPTCHA Enterprise integration config. */ + GoogleCloudIdentitytoolkitAdminV2RecaptchaConfig: { + /** @description The reCAPTCHA config for email/password provider, containing the enforcement status. The email/password provider contains all related user flows protected by reCAPTCHA. */ + emailPasswordEnforcementState?: + | "RECAPTCHA_PROVIDER_ENFORCEMENT_STATE_UNSPECIFIED" + | "OFF" + | "AUDIT" + | "ENFORCE"; + /** @description The managed rules for authentication action based on reCAPTCHA scores. The rules are shared across providers for a given tenant project. */ + managedRules?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2RecaptchaManagedRule"][]; + /** @description Output only. The reCAPTCHA keys. */ + recaptchaKeys?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2RecaptchaKey"][]; + /** @description Whether to use the account defender for reCAPTCHA assessment. Defaults to `false`. */ + useAccountDefender?: boolean; + }; + /** @description The reCAPTCHA key config. reCAPTCHA Enterprise offers different keys for different client platforms. */ + GoogleCloudIdentitytoolkitAdminV2RecaptchaKey: { + /** @description The reCAPTCHA Enterprise key resource name, e.g. "projects/{project}/keys/{key}" */ + key?: string; + /** @description The client's platform type. */ + type?: "CLIENT_TYPE_UNSPECIFIED" | "WEB" | "IOS" | "ANDROID"; + }; + /** @description The config for a reCAPTCHA managed rule. Models a single interval [start_score, end_score]. The start_score is implicit. It is either the closest smaller end_score (if one is available) or 0. Intervals in aggregate span [0, 1] without overlapping. */ + GoogleCloudIdentitytoolkitAdminV2RecaptchaManagedRule: { + /** @description The action taken if the reCAPTCHA score of a request is within the interval [start_score, end_score]. */ + action?: "RECAPTCHA_ACTION_UNSPECIFIED" | "BLOCK"; + /** + * Format: float + * @description The end score (inclusive) of the score range for an action. Must be a value between 0.0 and 1.0, at 11 discrete values; e.g. 0, 0.1, 0.2, 0.3, ... 0.9, 1.0. A score of 0.0 indicates the riskiest request (likely a bot), whereas 1.0 indicates the safest request (likely a human). See https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment. + */ + endScore?: number; + }; + /** @description Configuration for logging requests made to this project to Stackdriver Logging */ GoogleCloudIdentitytoolkitAdminV2RequestLogging: { - /** - * Whether logging is enabled for this project or not. - */ + /** @description Whether logging is enabled for this project or not. */ enabled?: boolean; }; - /** - * Options for email sending. - */ + /** @description Options for email sending. */ GoogleCloudIdentitytoolkitAdminV2SendEmail: { - /** - * action url in email template. - */ + /** @description action url in email template. */ callbackUri?: string; changeEmailTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailTemplate"]; dnsInfo?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DnsInfo"]; legacyResetPasswordTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailTemplate"]; - /** - * The method used for sending an email. - */ + /** @description The method used for sending an email. */ method?: "METHOD_UNSPECIFIED" | "DEFAULT" | "CUSTOM_SMTP"; resetPasswordTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailTemplate"]; revertSecondFactorAdditionTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailTemplate"]; smtp?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Smtp"]; verifyEmailTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailTemplate"]; }; - /** - * Options for SMS sending. - */ + /** @description Options for SMS sending. */ GoogleCloudIdentitytoolkitAdminV2SendSms: { smsTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SmsTemplate"]; - /** - * Whether to use the accept_language header for SMS. - */ + /** @description Whether to use the accept_language header for SMS. */ useDeviceLocale?: boolean; }; - /** - * Configuration related to local sign in methods. - */ + /** @description Configuration related to local sign in methods. */ GoogleCloudIdentitytoolkitAdminV2SignInConfig: { - /** - * Whether to allow more than one account to have the same email. - */ + /** @description Whether to allow more than one account to have the same email. */ allowDuplicateEmails?: boolean; anonymous?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Anonymous"]; email?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Email"]; hashConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2HashConfig"]; phoneNumber?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2PhoneNumber"]; }; - /** - * The template to use when sending an SMS. - */ + /** @description Configures the regions where users are allowed to send verification SMS for the project or tenant. This is based on the calling code of the destination phone number. */ + GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig: { + allowByDefault?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2AllowByDefault"]; + allowlistOnly?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2AllowlistOnly"]; + }; + /** @description The template to use when sending an SMS. */ GoogleCloudIdentitytoolkitAdminV2SmsTemplate: { - /** - * Output only. The SMS's content. Can contain the following placeholders which will be replaced with the appropriate values: %APP_NAME% - For Android or iOS apps, the app's display name. For web apps, the domain hosting the application. %LOGIN_CODE% - The OOB code being sent in the SMS. - */ + /** @description Output only. The SMS's content. Can contain the following placeholders which will be replaced with the appropriate values: %APP_NAME% - For Android or iOS apps, the app's display name. For web apps, the domain hosting the application. %LOGIN_CODE% - The OOB code being sent in the SMS. */ content?: string; }; - /** - * Configuration for SMTP relay - */ + /** @description Configuration for SMTP relay */ GoogleCloudIdentitytoolkitAdminV2Smtp: { - /** - * SMTP relay host - */ + /** @description SMTP relay host */ host?: string; - /** - * SMTP relay password - */ + /** @description SMTP relay password */ password?: string; /** - * SMTP relay port + * Format: int32 + * @description SMTP relay port */ port?: number; - /** - * SMTP security mode. - */ + /** @description SMTP security mode. */ securityMode?: "SECURITY_MODE_UNSPECIFIED" | "SSL" | "START_TLS"; - /** - * Sender email for the SMTP relay - */ + /** @description Sender email for the SMTP relay */ senderEmail?: string; - /** - * SMTP relay username - */ + /** @description SMTP relay username */ username?: string; }; - /** - * The SP's certificate data for IDP to verify the SAMLRequest generated by the SP. - */ + /** @description The SP's certificate data for IDP to verify the SAMLRequest generated by the SP. */ GoogleCloudIdentitytoolkitAdminV2SpCertificate: { /** - * Timestamp of the cert expiration instance. + * Format: google-datetime + * @description Timestamp of the cert expiration instance. */ expiresAt?: string; - /** - * Self-signed public certificate. - */ + /** @description Self-signed public certificate. */ x509Certificate?: string; }; - /** - * The SAML SP (Service Provider) configuration when the project acts as the relying party to receive and accept an authentication assertion issued by a SAML identity provider. - */ + /** @description The SAML SP (Service Provider) configuration when the project acts as the relying party to receive and accept an authentication assertion issued by a SAML identity provider. */ GoogleCloudIdentitytoolkitAdminV2SpConfig: { - /** - * Callback URI where responses from IDP are handled. - */ + /** @description Callback URI where responses from IDP are handled. */ callbackUri?: string; - /** - * Output only. Public certificates generated by the server to verify the signature in SAMLRequest in the SP-initiated flow. - */ + /** @description Output only. Public certificates generated by the server to verify the signature in SAMLRequest in the SP-initiated flow. */ spCertificates?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SpCertificate"][]; - /** - * Unique identifier for all SAML entities. - */ + /** @description Unique identifier for all SAML entities. */ spEntityId?: string; }; - /** - * Temporary quota increase / decrease - */ + /** @description Temporary quota increase / decrease */ GoogleCloudIdentitytoolkitAdminV2TemporaryQuota: { /** - * Corresponds to the 'refill_token_count' field in QuotaServer config + * Format: int64 + * @description Corresponds to the 'refill_token_count' field in QuotaServer config */ quota?: string; /** - * How long this quota will be active for + * Format: google-duration + * @description How long this quota will be active for */ quotaDuration?: string; /** - * When this quota will take affect + * Format: google-datetime + * @description When this quota will take affect */ startTime?: string; }; - /** - * A Tenant contains configuration for the tenant in a multi-tenant project. - */ + /** @description A Tenant contains configuration for the tenant in a multi-tenant project. */ GoogleCloudIdentitytoolkitAdminV2Tenant: { - /** - * Whether to allow email/password user authentication. - */ + /** @description Whether to allow email/password user authentication. */ allowPasswordSignup?: boolean; - /** - * Whether authentication is disabled for the tenant. If true, the users under the disabled tenant are not allowed to sign-in. Admins of the disabled tenant are not able to manage its users. - */ + /** @description Whether anonymous users will be auto-deleted after a period of 30 days. */ + autodeleteAnonymousUsers?: boolean; + client?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ClientPermissionConfig"]; + /** @description Whether authentication is disabled for the tenant. If true, the users under the disabled tenant are not allowed to sign-in. Admins of the disabled tenant are not able to manage its users. */ disableAuth?: boolean; - /** - * Display name of the tenant. - */ + /** @description Display name of the tenant. */ displayName?: string; - /** - * Whether to enable anonymous user authentication. - */ + emailPrivacyConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig"]; + /** @description Whether to enable anonymous user authentication. */ enableAnonymousUser?: boolean; - /** - * Whether to enable email link user authentication. - */ + /** @description Whether to enable email link user authentication. */ enableEmailLinkSignin?: boolean; hashConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2HashConfig"]; inheritance?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Inheritance"]; mfaConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig"]; - /** - * Output only. Resource name of a tenant. For example: "projects/{project-id}/tenants/{tenant-id}" - */ + monitoring?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2MonitoringConfig"]; + /** @description Output only. Resource name of a tenant. For example: "projects/{project-id}/tenants/{tenant-id}" */ name?: string; - /** - * A map of pairs that can be used for MFA. The phone number should be in E.164 format (https://www.itu.int/rec/T-REC-E.164/) and a maximum of 10 pairs can be added (error will be thrown once exceeded). - */ + passwordPolicyConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2PasswordPolicyConfig"]; + recaptchaConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2RecaptchaConfig"]; + smsRegionConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig"]; + /** @description A map of pairs that can be used for MFA. The phone number should be in E.164 format (https://www.itu.int/rec/T-REC-E.164/) and a maximum of 10 pairs can be added (error will be thrown once exceeded). */ testPhoneNumbers?: { [key: string]: string }; }; - /** - * Synchronous Cloud Function with HTTP Trigger - */ - GoogleCloudIdentitytoolkitAdminV2Trigger: { + /** @description TotpMFAProviderConfig represents the TOTP based MFA provider. */ + GoogleCloudIdentitytoolkitAdminV2TotpMfaProviderConfig: { /** - * HTTP URI trigger for the Cloud Function. + * Format: int32 + * @description The allowed number of adjacent intervals that will be used for verification to avoid clock skew. */ + adjacentIntervals?: number; + }; + /** @description Synchronous Cloud Function with HTTP Trigger */ + GoogleCloudIdentitytoolkitAdminV2Trigger: { + /** @description HTTP URI trigger for the Cloud Function. */ functionUri?: string; /** - * When the trigger was changed. + * Format: google-datetime + * @description When the trigger was changed. */ updateTime?: string; }; - /** - * The information required to auto-retrieve an SMS. - */ + /** @description The information required to auto-retrieve an SMS. */ GoogleCloudIdentitytoolkitV2AutoRetrievalInfo: { - /** - * The Android app's signature hash for Google Play Service's SMS Retriever API. - */ + /** @description The Android app's signature hash for Google Play Service's SMS Retriever API. */ appSignatureHash?: string; }; - /** - * Finishes enrolling a second factor for the user. - */ - GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentRequest: { + /** @description Custom strength options to enforce on user passwords. */ + GoogleCloudIdentitytoolkitV2CustomStrengthOptions: { + /** @description The password must contain a lower case character. */ + containsLowercaseCharacter?: boolean; + /** @description The password must contain a non alpha numeric character. */ + containsNonAlphanumericCharacter?: boolean; + /** @description The password must contain a number. */ + containsNumericCharacter?: boolean; + /** @description The password must contain an upper case character. */ + containsUppercaseCharacter?: boolean; /** - * Display name which is entered by users to distinguish between different second factors with same type or different type. + * Format: int32 + * @description Maximum password length. No default max length */ - displayName?: string; + maxPasswordLength?: number; /** - * Required. ID token. + * Format: int32 + * @description Minimum password length. Range from 6 to 30 */ + minPasswordLength?: number; + }; + /** @description Finishes enrolling a second factor for the user. */ + GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentRequest: { + /** @description Display name which is entered by users to distinguish between different second factors with same type or different type. */ + displayName?: string; + /** @description Required. ID token. */ idToken?: string; phoneVerificationInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaPhoneRequestInfo"]; - /** - * The ID of the Identity Platform tenant that the user enrolling MFA belongs to. If not set, the user belongs to the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant that the user enrolling MFA belongs to. If not set, the user belongs to the default Identity Platform project. */ tenantId?: string; + totpVerificationInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaTotpEnrollmentRequestInfo"]; }; - /** - * FinalizeMfaEnrollment response. - */ + /** @description FinalizeMfaEnrollment response. */ GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentResponse: { - /** - * ID token updated to reflect MFA enrollment. - */ + /** @description ID token updated to reflect MFA enrollment. */ idToken?: string; phoneAuthInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaPhoneResponseInfo"]; - /** - * Refresh token updated to reflect MFA enrollment. - */ + /** @description Refresh token updated to reflect MFA enrollment. */ refreshToken?: string; + totpAuthInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaTotpEnrollmentResponseInfo"]; }; - /** - * Phone Verification info for a FinalizeMfa request. - */ + /** @description Phone Verification info for a FinalizeMfa request. */ GoogleCloudIdentitytoolkitV2FinalizeMfaPhoneRequestInfo: { - /** - * Android only. Uses for "instant" phone number verification though GmsCore. - */ + /** @description Android only. Uses for "instant" phone number verification though GmsCore. */ androidVerificationProof?: string; - /** - * User-entered verification code. - */ + /** @description User-entered verification code. */ code?: string; - /** - * Required if Android verification proof is presented. - */ + /** @description Required if Android verification proof is presented. */ phoneNumber?: string; - /** - * An opaque string that represents the enrollment session. - */ + /** @description An opaque string that represents the enrollment session. */ sessionInfo?: string; }; - /** - * Phone Verification info for a FinalizeMfa response. - */ + /** @description Phone Verification info for a FinalizeMfa response. */ GoogleCloudIdentitytoolkitV2FinalizeMfaPhoneResponseInfo: { - /** - * Android only. Long-lived replacement for valid code tied to android device. - */ + /** @description Android only. Long-lived replacement for valid code tied to android device. */ androidVerificationProof?: string; /** - * Android only. Expiration time of verification proof in seconds. + * Format: google-datetime + * @description Android only. Expiration time of verification proof in seconds. */ androidVerificationProofExpireTime?: string; - /** - * For Android verification proof. - */ + /** @description For Android verification proof. */ phoneNumber?: string; }; - /** - * Finalizes sign-in by verifying MFA challenge. - */ + /** @description Finalizes sign-in by verifying MFA challenge. */ GoogleCloudIdentitytoolkitV2FinalizeMfaSignInRequest: { - /** - * Required. Pending credential from first factor sign-in. - */ + /** @description The MFA enrollment ID from the user's list of current MFA enrollments. */ + mfaEnrollmentId?: string; + /** @description Required. Pending credential from first factor sign-in. */ mfaPendingCredential?: string; phoneVerificationInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaPhoneRequestInfo"]; - /** - * The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. */ tenantId?: string; + totpVerificationInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2MfaTotpSignInRequestInfo"]; }; - /** - * FinalizeMfaSignIn response. - */ + /** @description FinalizeMfaSignIn response. */ GoogleCloudIdentitytoolkitV2FinalizeMfaSignInResponse: { - /** - * ID token for the authenticated user. - */ + /** @description ID token for the authenticated user. */ idToken?: string; phoneAuthInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaPhoneResponseInfo"]; - /** - * Refresh token for the authenticated user. - */ + /** @description Refresh token for the authenticated user. */ refreshToken?: string; }; - /** - * Sends MFA enrollment verification SMS for a user. - */ + /** @description Mfa request info specific to TOTP auth for FinalizeMfa. */ + GoogleCloudIdentitytoolkitV2FinalizeMfaTotpEnrollmentRequestInfo: { + /** @description An opaque string that represents the enrollment session. */ + sessionInfo?: string; + /** @description User-entered verification code. */ + verificationCode?: string; + }; + /** @description Mfa response info specific to TOTP auth for FinalizeMfa. */ + GoogleCloudIdentitytoolkitV2FinalizeMfaTotpEnrollmentResponseInfo: { [key: string]: unknown }; + /** @description TOTP verification info for FinalizeMfaSignInRequest. */ + GoogleCloudIdentitytoolkitV2MfaTotpSignInRequestInfo: { + /** @description User-entered verification code. */ + verificationCode?: string; + }; + /** @description Configuration for password policy. */ + GoogleCloudIdentitytoolkitV2PasswordPolicy: { + /** @description Output only. Allowed characters which satisfy the non_alphanumeric requirement. */ + allowedNonAlphanumericCharacters?: string[]; + customStrengthOptions?: components["schemas"]["GoogleCloudIdentitytoolkitV2CustomStrengthOptions"]; + /** @description Output only. Which enforcement mode to use for the password policy. */ + enforcementState?: "ENFORCEMENT_STATE_UNSPECIFIED" | "OFF" | "ENFORCE"; + /** @description Users must have a password compliant with the password policy to sign-in. */ + forceUpgradeOnSignin?: boolean; + /** + * Format: int32 + * @description Output only. schema version number for the password policy + */ + schemaVersion?: number; + }; + /** @description Configuration for reCAPTCHA */ + GoogleCloudIdentitytoolkitV2RecaptchaConfig: { + /** @description The reCAPTCHA enforcement state for the providers that GCIP supports reCAPTCHA protection. */ + recaptchaEnforcementState?: components["schemas"]["GoogleCloudIdentitytoolkitV2RecaptchaEnforcementState"][]; + /** @description The reCAPTCHA Enterprise key resource name, e.g. "projects/{project}/keys/{key}". This will only be returned when the reCAPTCHA enforcement state is AUDIT or ENFORCE on at least one of the reCAPTCHA providers. */ + recaptchaKey?: string; + }; + /** @description Enforcement states for reCAPTCHA protection. */ + GoogleCloudIdentitytoolkitV2RecaptchaEnforcementState: { + /** @description The reCAPTCHA enforcement state for the provider. */ + enforcementState?: "ENFORCEMENT_STATE_UNSPECIFIED" | "OFF" | "AUDIT" | "ENFORCE"; + /** @description The provider that has reCAPTCHA protection. */ + provider?: "RECAPTCHA_PROVIDER_UNSPECIFIED" | "EMAIL_PASSWORD_PROVIDER"; + }; + /** @description Request message for RevokeToken. */ + GoogleCloudIdentitytoolkitV2RevokeTokenRequest: { + /** @description Required. A valid Identity Platform ID token to link the account. If there was a successful token revocation request on the account and no tokens are generated after the revocation, the duplicate requests will be ignored and returned immediately. */ + idToken?: string; + /** @description Required. The idp provider for the token. Currently only supports Apple Idp. The format should be "apple.com". */ + providerId?: string; + /** @description The redirect URI provided in the initial authorization request made by the client to the IDP. The URI must use the HTTPS protocol, include a domain name, and can't contain an IP address or localhost. Required if token_type is CODE. */ + redirectUri?: string; + /** @description The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. */ + tenantId?: string; + /** @description Required. The token to be revoked. If an authorization_code is passed in, the API will first exchange the code for access token and then revoke the token exchanged. */ + token?: string; + /** @description Required. The type of the token to be revoked. */ + tokenType?: "TOKEN_TYPE_UNSPECIFIED" | "REFRESH_TOKEN" | "ACCESS_TOKEN" | "CODE"; + }; + /** @description Response message for RevokeToken. Empty for now. */ + GoogleCloudIdentitytoolkitV2RevokeTokenResponse: { [key: string]: unknown }; + /** @description Sends MFA enrollment verification SMS for a user. */ GoogleCloudIdentitytoolkitV2StartMfaEnrollmentRequest: { - /** - * Required. User's ID token. - */ + /** @description Required. User's ID token. */ idToken?: string; phoneEnrollmentInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaPhoneRequestInfo"]; - /** - * The ID of the Identity Platform tenant that the user enrolling MFA belongs to. If not set, the user belongs to the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant that the user enrolling MFA belongs to. If not set, the user belongs to the default Identity Platform project. */ tenantId?: string; + totpEnrollmentInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaTotpEnrollmentRequestInfo"]; }; - /** - * StartMfaEnrollment response. - */ + /** @description StartMfaEnrollment response. */ GoogleCloudIdentitytoolkitV2StartMfaEnrollmentResponse: { phoneSessionInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaPhoneResponseInfo"]; + totpSessionInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaTotpEnrollmentResponseInfo"]; }; - /** - * App Verification info for a StartMfa request. - */ + /** @description App Verification info for a StartMfa request. */ GoogleCloudIdentitytoolkitV2StartMfaPhoneRequestInfo: { autoRetrievalInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2AutoRetrievalInfo"]; - /** - * iOS only. Receipt of successful app token validation with APNS. - */ + /** @description iOS only. Receipt of successful app token validation with APNS. */ iosReceipt?: string; - /** - * iOS only. Secret delivered to iOS app via APNS. - */ + /** @description iOS only. Secret delivered to iOS app via APNS. */ iosSecret?: string; - /** - * Required for enrollment. Phone number to be enrolled as MFA. - */ + /** @description Required for enrollment. Phone number to be enrolled as MFA. */ phoneNumber?: string; - /** - * Web only. Recaptcha solution. - */ + /** @description Android only. Used to assert application identity in place of a recaptcha token (or safety net token). A Play Integrity Token can be generated via the [PlayIntegrity API] (https://developer.android.com/google/play/integrity) with applying SHA256 to the `phone_number` field as the nonce. */ + playIntegrityToken?: string; + /** @description Web only. Recaptcha solution. */ recaptchaToken?: string; - /** - * Android only. Used to assert application identity in place of a recaptcha token. A SafetyNet Token can be generated via the [SafetyNet Android Attestation API](https://developer.android.com/training/safetynet/attestation.html), with the Base64 encoding of the `phone_number` field as the nonce. - */ + /** @description Android only. Used to assert application identity in place of a recaptcha token. A SafetyNet Token can be generated via the [SafetyNet Android Attestation API](https://developer.android.com/training/safetynet/attestation.html), with the Base64 encoding of the `phone_number` field as the nonce. */ safetyNetToken?: string; }; - /** - * Phone Verification info for a StartMfa response. - */ + /** @description Phone Verification info for a StartMfa response. */ GoogleCloudIdentitytoolkitV2StartMfaPhoneResponseInfo: { - /** - * An opaque string that represents the enrollment session. - */ + /** @description An opaque string that represents the enrollment session. */ sessionInfo?: string; }; - /** - * Starts multi-factor sign-in by sending the multi-factor auth challenge. - */ + /** @description Starts multi-factor sign-in by sending the multi-factor auth challenge. */ GoogleCloudIdentitytoolkitV2StartMfaSignInRequest: { - /** - * Required. MFA enrollment id from the user's list of current MFA enrollments. - */ + /** @description Required. MFA enrollment id from the user's list of current MFA enrollments. */ mfaEnrollmentId?: string; - /** - * Required. Pending credential from first factor sign-in. - */ + /** @description Required. Pending credential from first factor sign-in. */ mfaPendingCredential?: string; phoneSignInInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaPhoneRequestInfo"]; - /** - * The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. - */ + /** @description The ID of the Identity Platform tenant the user is signing in to. If not set, the user will sign in to the default Identity Platform project. */ tenantId?: string; }; - /** - * StartMfaSignIn response. - */ + /** @description StartMfaSignIn response. */ GoogleCloudIdentitytoolkitV2StartMfaSignInResponse: { phoneResponseInfo?: components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaPhoneResponseInfo"]; }; - /** - * Withdraws MFA. - */ - GoogleCloudIdentitytoolkitV2WithdrawMfaRequest: { + /** @description Mfa request info specific to TOTP auth for StartMfa. */ + GoogleCloudIdentitytoolkitV2StartMfaTotpEnrollmentRequestInfo: { [key: string]: unknown }; + /** @description Mfa response info specific to TOTP auth for StartMfa. */ + GoogleCloudIdentitytoolkitV2StartMfaTotpEnrollmentResponseInfo: { /** - * Required. User's ID token. + * Format: google-datetime + * @description The time by which the enrollment must finish. */ - idToken?: string; + finalizeEnrollmentTime?: string; + /** @description The hashing algorithm used to generate the verification code. */ + hashingAlgorithm?: string; /** - * Required. MFA enrollment id from a current MFA enrollment. + * Format: int32 + * @description Duration in seconds at which the verification code will change. */ - mfaEnrollmentId?: string; + periodSec?: number; + /** @description An encoded string that represents the enrollment session. */ + sessionInfo?: string; + /** @description A base 32 encoded string that represents the shared TOTP secret. The base 32 encoding is the one specified by [RFC4648#section-6](https://datatracker.ietf.org/doc/html/rfc4648#section-6). (This is the same as the base 32 encoding from [RFC3548#section-5](https://datatracker.ietf.org/doc/html/rfc3548#section-5).) */ + sharedSecretKey?: string; /** - * The ID of the Identity Platform tenant that the user unenrolling MFA belongs to. If not set, the user belongs to the default Identity Platform project. + * Format: int32 + * @description The length of the verification code that needs to be generated. */ + verificationCodeLength?: number; + }; + /** @description Withdraws MFA. */ + GoogleCloudIdentitytoolkitV2WithdrawMfaRequest: { + /** @description Required. User's ID token. */ + idToken?: string; + /** @description Required. MFA enrollment id from a current MFA enrollment. */ + mfaEnrollmentId?: string; + /** @description The ID of the Identity Platform tenant that the user unenrolling MFA belongs to. If not set, the user belongs to the default Identity Platform project. */ tenantId?: string; }; - /** - * Withdraws MultiFactorAuth response. - */ + /** @description Withdraws MultiFactorAuth response. */ GoogleCloudIdentitytoolkitV2WithdrawMfaResponse: { - /** - * ID token updated to reflect removal of the second factor. - */ + /** @description ID token updated to reflect removal of the second factor. */ idToken?: string; - /** - * Refresh token updated to reflect removal of the second factor. - */ + /** @description Refresh token updated to reflect removal of the second factor. */ refreshToken?: string; }; - /** - * Specifies the audit configuration for a service. The configuration determines which permission types are logged, and what identities, if any, are exempted from logging. An AuditConfig must have one or more AuditLogConfigs. If there are AuditConfigs for both `allServices` and a specific service, the union of the two AuditConfigs is used for that service: the log_types specified in each AuditConfig are enabled, and the exempted_members in each AuditLogConfig are exempted. Example Policy with multiple AuditConfigs: { "audit_configs": [ { "service": "allServices", "audit_log_configs": [ { "log_type": "DATA_READ", "exempted_members": [ "user:jose@example.com" ] }, { "log_type": "DATA_WRITE" }, { "log_type": "ADMIN_READ" } ] }, { "service": "sampleservice.googleapis.com", "audit_log_configs": [ { "log_type": "DATA_READ" }, { "log_type": "DATA_WRITE", "exempted_members": [ "user:aliya@example.com" ] } ] } ] } For sampleservice, this policy enables DATA_READ, DATA_WRITE and ADMIN_READ logging. It also exempts jose@example.com from DATA_READ logging, and aliya@example.com from DATA_WRITE logging. - */ + /** @description Specifies the audit configuration for a service. The configuration determines which permission types are logged, and what identities, if any, are exempted from logging. An AuditConfig must have one or more AuditLogConfigs. If there are AuditConfigs for both `allServices` and a specific service, the union of the two AuditConfigs is used for that service: the log_types specified in each AuditConfig are enabled, and the exempted_members in each AuditLogConfig are exempted. Example Policy with multiple AuditConfigs: { "audit_configs": [ { "service": "allServices", "audit_log_configs": [ { "log_type": "DATA_READ", "exempted_members": [ "user:jose@example.com" ] }, { "log_type": "DATA_WRITE" }, { "log_type": "ADMIN_READ" } ] }, { "service": "sampleservice.googleapis.com", "audit_log_configs": [ { "log_type": "DATA_READ" }, { "log_type": "DATA_WRITE", "exempted_members": [ "user:aliya@example.com" ] } ] } ] } For sampleservice, this policy enables DATA_READ, DATA_WRITE and ADMIN_READ logging. It also exempts `jose@example.com` from DATA_READ logging, and `aliya@example.com` from DATA_WRITE logging. */ GoogleIamV1AuditConfig: { - /** - * The configuration for logging of each type of permission. - */ + /** @description The configuration for logging of each type of permission. */ auditLogConfigs?: components["schemas"]["GoogleIamV1AuditLogConfig"][]; - /** - * Specifies a service that will be enabled for audit logging. For example, `storage.googleapis.com`, `cloudsql.googleapis.com`. `allServices` is a special value that covers all services. - */ + /** @description Specifies a service that will be enabled for audit logging. For example, `storage.googleapis.com`, `cloudsql.googleapis.com`. `allServices` is a special value that covers all services. */ service?: string; }; - /** - * Provides the configuration for logging a type of permissions. Example: { "audit_log_configs": [ { "log_type": "DATA_READ", "exempted_members": [ "user:jose@example.com" ] }, { "log_type": "DATA_WRITE" } ] } This enables 'DATA_READ' and 'DATA_WRITE' logging, while exempting jose@example.com from DATA_READ logging. - */ + /** @description Provides the configuration for logging a type of permissions. Example: { "audit_log_configs": [ { "log_type": "DATA_READ", "exempted_members": [ "user:jose@example.com" ] }, { "log_type": "DATA_WRITE" } ] } This enables 'DATA_READ' and 'DATA_WRITE' logging, while exempting jose@example.com from DATA_READ logging. */ GoogleIamV1AuditLogConfig: { - /** - * Specifies the identities that do not cause logging for this type of permission. Follows the same format of Binding.members. - */ + /** @description Specifies the identities that do not cause logging for this type of permission. Follows the same format of Binding.members. */ exemptedMembers?: string[]; - /** - * The log type that this config enables. - */ + /** @description The log type that this config enables. */ logType?: "LOG_TYPE_UNSPECIFIED" | "ADMIN_READ" | "DATA_WRITE" | "DATA_READ"; }; - /** - * Associates `members`, or principals, with a `role`. - */ + /** @description Associates `members`, or principals, with a `role`. */ GoogleIamV1Binding: { condition?: components["schemas"]["GoogleTypeExpr"]; - /** - * Specifies the principals requesting access for a Cloud Platform resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. - */ + /** @description Specifies the principals requesting access for a Google Cloud resource. `members` can have the following values: * `allUsers`: A special identifier that represents anyone who is on the internet; with or without a Google account. * `allAuthenticatedUsers`: A special identifier that represents anyone who is authenticated with a Google account or a service account. Does not include identities that come from external identity providers (IdPs) through identity federation. * `user:{emailid}`: An email address that represents a specific Google account. For example, `alice@example.com` . * `serviceAccount:{emailid}`: An email address that represents a Google service account. For example, `my-other-app@appspot.gserviceaccount.com`. * `serviceAccount:{projectid}.svc.id.goog[{namespace}/{kubernetes-sa}]`: An identifier for a [Kubernetes service account](https://cloud.google.com/kubernetes-engine/docs/how-to/kubernetes-service-accounts). For example, `my-project.svc.id.goog[my-namespace/my-kubernetes-sa]`. * `group:{emailid}`: An email address that represents a Google group. For example, `admins@example.com`. * `domain:{domain}`: The G Suite domain (primary) that represents all the users of that domain. For example, `google.com` or `example.com`. * `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a user that has been recently deleted. For example, `alice@example.com?uid=123456789012345678901`. If the user is recovered, this value reverts to `user:{emailid}` and the recovered user retains the role in the binding. * `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a service account that has been recently deleted. For example, `my-other-app@appspot.gserviceaccount.com?uid=123456789012345678901`. If the service account is undeleted, this value reverts to `serviceAccount:{emailid}` and the undeleted service account retains the role in the binding. * `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier) representing a Google group that has been recently deleted. For example, `admins@example.com?uid=123456789012345678901`. If the group is recovered, this value reverts to `group:{emailid}` and the recovered group retains the role in the binding. */ members?: string[]; - /** - * Role that is assigned to the list of `members`, or principals. For example, `roles/viewer`, `roles/editor`, or `roles/owner`. - */ + /** @description Role that is assigned to the list of `members`, or principals. For example, `roles/viewer`, `roles/editor`, or `roles/owner`. */ role?: string; }; - /** - * Request message for `GetIamPolicy` method. - */ + /** @description Request message for `GetIamPolicy` method. */ GoogleIamV1GetIamPolicyRequest: { options?: components["schemas"]["GoogleIamV1GetPolicyOptions"]; }; - /** - * Encapsulates settings provided to GetIamPolicy. - */ + /** @description Encapsulates settings provided to GetIamPolicy. */ GoogleIamV1GetPolicyOptions: { /** - * Optional. The policy format version to be returned. Valid values are 0, 1, and 3. Requests specifying an invalid value will be rejected. Requests for policies with any conditional bindings must specify version 3. Policies without any conditional bindings may specify any valid value or leave the field unset. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). + * Format: int32 + * @description Optional. The maximum policy version that will be used to format the policy. Valid values are 0, 1, and 3. Requests specifying an invalid value will be rejected. Requests for policies with any conditional role bindings must specify version 3. Policies with no conditional role bindings may specify any valid value or leave the field unset. The policy in the response might use the policy version that you specified, or it might use a lower policy version. For example, if you specify version 3, but the policy has no conditional role bindings, the response uses version 1. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). */ requestedPolicyVersion?: number; }; - /** - * An Identity and Access Management (IAM) policy, which specifies access controls for Google Cloud resources. A `Policy` is a collection of `bindings`. A `binding` binds one or more `members`, or principals, to a single `role`. Principals can be user accounts, service accounts, Google groups, and domains (such as G Suite). A `role` is a named list of permissions; each `role` can be an IAM predefined role or a user-created custom role. For some types of Google Cloud resources, a `binding` can also specify a `condition`, which is a logical expression that allows access to a resource only if the expression evaluates to `true`. A condition can add constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 } **YAML example:** bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3 For a description of IAM and its features, see the [IAM documentation](https://cloud.google.com/iam/docs/). - */ + /** @description An Identity and Access Management (IAM) policy, which specifies access controls for Google Cloud resources. A `Policy` is a collection of `bindings`. A `binding` binds one or more `members`, or principals, to a single `role`. Principals can be user accounts, service accounts, Google groups, and domains (such as G Suite). A `role` is a named list of permissions; each `role` can be an IAM predefined role or a user-created custom role. For some types of Google Cloud resources, a `binding` can also specify a `condition`, which is a logical expression that allows access to a resource only if the expression evaluates to `true`. A condition can add constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 } **YAML example:** bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3 For a description of IAM and its features, see the [IAM documentation](https://cloud.google.com/iam/docs/). */ GoogleIamV1Policy: { - /** - * Specifies cloud audit logging configuration for this policy. - */ + /** @description Specifies cloud audit logging configuration for this policy. */ auditConfigs?: components["schemas"]["GoogleIamV1AuditConfig"][]; - /** - * Associates a list of `members`, or principals, with a `role`. Optionally, may specify a `condition` that determines how and when the `bindings` are applied. Each of the `bindings` must contain at least one principal. The `bindings` in a `Policy` can refer to up to 1,500 principals; up to 250 of these principals can be Google groups. Each occurrence of a principal counts towards these limits. For example, if the `bindings` grant 50 different roles to `user:alice@example.com`, and not to any other principal, then you can add another 1,450 principals to the `bindings` in the `Policy`. - */ + /** @description Associates a list of `members`, or principals, with a `role`. Optionally, may specify a `condition` that determines how and when the `bindings` are applied. Each of the `bindings` must contain at least one principal. The `bindings` in a `Policy` can refer to up to 1,500 principals; up to 250 of these principals can be Google groups. Each occurrence of a principal counts towards these limits. For example, if the `bindings` grant 50 different roles to `user:alice@example.com`, and not to any other principal, then you can add another 1,450 principals to the `bindings` in the `Policy`. */ bindings?: components["schemas"]["GoogleIamV1Binding"][]; /** - * `etag` is used for optimistic concurrency control as a way to help prevent simultaneous updates of a policy from overwriting each other. It is strongly suggested that systems make use of the `etag` in the read-modify-write cycle to perform policy updates in order to avoid race conditions: An `etag` is returned in the response to `getIamPolicy`, and systems are expected to put that etag in the request to `setIamPolicy` to ensure that their change will be applied to the same version of the policy. **Important:** If you use IAM Conditions, you must include the `etag` field whenever you call `setIamPolicy`. If you omit this field, then IAM allows you to overwrite a version `3` policy with a version `1` policy, and all of the conditions in the version `3` policy are lost. + * Format: byte + * @description `etag` is used for optimistic concurrency control as a way to help prevent simultaneous updates of a policy from overwriting each other. It is strongly suggested that systems make use of the `etag` in the read-modify-write cycle to perform policy updates in order to avoid race conditions: An `etag` is returned in the response to `getIamPolicy`, and systems are expected to put that etag in the request to `setIamPolicy` to ensure that their change will be applied to the same version of the policy. **Important:** If you use IAM Conditions, you must include the `etag` field whenever you call `setIamPolicy`. If you omit this field, then IAM allows you to overwrite a version `3` policy with a version `1` policy, and all of the conditions in the version `3` policy are lost. */ etag?: string; /** - * Specifies the format of the policy. Valid values are `0`, `1`, and `3`. Requests that specify an invalid value are rejected. Any operation that affects conditional role bindings must specify version `3`. This requirement applies to the following operations: * Getting a policy that includes a conditional role binding * Adding a conditional role binding to a policy * Changing a conditional role binding in a policy * Removing any role binding, with or without a condition, from a policy that includes conditions **Important:** If you use IAM Conditions, you must include the `etag` field whenever you call `setIamPolicy`. If you omit this field, then IAM allows you to overwrite a version `3` policy with a version `1` policy, and all of the conditions in the version `3` policy are lost. If a policy does not include any conditions, operations on that policy may specify any valid version or leave the field unset. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). + * Format: int32 + * @description Specifies the format of the policy. Valid values are `0`, `1`, and `3`. Requests that specify an invalid value are rejected. Any operation that affects conditional role bindings must specify version `3`. This requirement applies to the following operations: * Getting a policy that includes a conditional role binding * Adding a conditional role binding to a policy * Changing a conditional role binding in a policy * Removing any role binding, with or without a condition, from a policy that includes conditions **Important:** If you use IAM Conditions, you must include the `etag` field whenever you call `setIamPolicy`. If you omit this field, then IAM allows you to overwrite a version `3` policy with a version `1` policy, and all of the conditions in the version `3` policy are lost. If a policy does not include any conditions, operations on that policy may specify any valid version or leave the field unset. To learn which resources support conditions in their IAM policies, see the [IAM documentation](https://cloud.google.com/iam/help/conditions/resource-policies). */ version?: number; }; - /** - * Request message for `SetIamPolicy` method. - */ + /** @description Request message for `SetIamPolicy` method. */ GoogleIamV1SetIamPolicyRequest: { policy?: components["schemas"]["GoogleIamV1Policy"]; /** - * OPTIONAL: A FieldMask specifying which fields of the policy to modify. Only the fields in the mask will be modified. If no mask is provided, the following default mask is used: `paths: "bindings, etag"` + * Format: google-fieldmask + * @description OPTIONAL: A FieldMask specifying which fields of the policy to modify. Only the fields in the mask will be modified. If no mask is provided, the following default mask is used: `paths: "bindings, etag"` */ updateMask?: string; }; - /** - * Request message for `TestIamPermissions` method. - */ + /** @description Request message for `TestIamPermissions` method. */ GoogleIamV1TestIamPermissionsRequest: { - /** - * The set of permissions to check for the `resource`. Permissions with wildcards (such as '*' or 'storage.*') are not allowed. For more information see [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). - */ + /** @description The set of permissions to check for the `resource`. Permissions with wildcards (such as `*` or `storage.*`) are not allowed. For more information see [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). */ permissions?: string[]; }; - /** - * Response message for `TestIamPermissions` method. - */ + /** @description Response message for `TestIamPermissions` method. */ GoogleIamV1TestIamPermissionsResponse: { - /** - * A subset of `TestPermissionsRequest.permissions` that the caller is allowed. - */ + /** @description A subset of `TestPermissionsRequest.permissions` that the caller is allowed. */ permissions?: string[]; }; - /** - * A generic empty message that you can re-use to avoid defining duplicated empty messages in your APIs. A typical example is to use it as the request or the response type of an API method. For instance: service Foo { rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); } The JSON representation for `Empty` is empty JSON object `{}`. - */ - GoogleProtobufEmpty: { [key: string]: any }; - /** - * Represents a textual expression in the Common Expression Language (CEL) syntax. CEL is a C-like expression language. The syntax and semantics of CEL are documented at https://github.com/google/cel-spec. Example (Comparison): title: "Summary size limit" description: "Determines if a summary is less than 100 chars" expression: "document.summary.size() < 100" Example (Equality): title: "Requestor is owner" description: "Determines if requestor is the document owner" expression: "document.owner == request.auth.claims.email" Example (Logic): title: "Public documents" description: "Determine whether the document should be publicly visible" expression: "document.type != 'private' && document.type != 'internal'" Example (Data Manipulation): title: "Notification string" description: "Create a notification string with a timestamp." expression: "'New message received at ' + string(document.create_time)" The exact variables and functions that may be referenced within an expression are determined by the service that evaluates it. See the service documentation for additional information. - */ + /** @description A generic empty message that you can re-use to avoid defining duplicated empty messages in your APIs. A typical example is to use it as the request or the response type of an API method. For instance: service Foo { rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); } */ + GoogleProtobufEmpty: { [key: string]: unknown }; + /** @description Represents a textual expression in the Common Expression Language (CEL) syntax. CEL is a C-like expression language. The syntax and semantics of CEL are documented at https://github.com/google/cel-spec. Example (Comparison): title: "Summary size limit" description: "Determines if a summary is less than 100 chars" expression: "document.summary.size() < 100" Example (Equality): title: "Requestor is owner" description: "Determines if requestor is the document owner" expression: "document.owner == request.auth.claims.email" Example (Logic): title: "Public documents" description: "Determine whether the document should be publicly visible" expression: "document.type != 'private' && document.type != 'internal'" Example (Data Manipulation): title: "Notification string" description: "Create a notification string with a timestamp." expression: "'New message received at ' + string(document.create_time)" The exact variables and functions that may be referenced within an expression are determined by the service that evaluates it. See the service documentation for additional information. */ GoogleTypeExpr: { - /** - * Optional. Description of the expression. This is a longer text which describes the expression, e.g. when hovered over it in a UI. - */ + /** @description Optional. Description of the expression. This is a longer text which describes the expression, e.g. when hovered over it in a UI. */ description?: string; - /** - * Textual representation of an expression in Common Expression Language syntax. - */ + /** @description Textual representation of an expression in Common Expression Language syntax. */ expression?: string; - /** - * Optional. String indicating the location of the expression for error reporting, e.g. a file name and a position in the file. - */ + /** @description Optional. String indicating the location of the expression for error reporting, e.g. a file name and a position in the file. */ location?: string; - /** - * Optional. Title for the expression, i.e. a short string describing its purpose. This can be used e.g. in UIs which allow to enter the expression. - */ + /** @description Optional. Title for the expression, i.e. a short string describing its purpose. This can be used e.g. in UIs which allow to enter the expression. */ title?: string; }; GrantTokenRequest: { /** - * ID token to exchange for an access token and a refresh token. This field is called `code` to conform with the OAuth 2.0 specification. This field is deprecated and is ignored. + * @deprecated + * @description ID token to exchange for an access token and a refresh token. This field is called `code` to conform with the OAuth 2.0 specification. This field is deprecated and is ignored. */ code?: string; - /** - * The grant_types that are supported: - `refresh_token` to exchange a Identity Platform refresh_token for Identity Platform id_token/access_token and possibly a new Identity Platform refresh_token. - */ + /** @description The grant_types that are supported: - `refresh_token` to exchange a Identity Platform refresh_token for Identity Platform id_token/access_token and possibly a new Identity Platform refresh_token. */ grantType?: string; - /** - * Identity Platform refresh_token. This field is ignored if `grantType` isn't `refresh_token`. - */ + /** @description Identity Platform refresh_token. This field is ignored if `grantType` isn't `refresh_token`. */ refreshToken?: string; }; GrantTokenResponse: { - /** - * DEPRECATED The granted access token. - */ + /** @description DEPRECATED The granted access token. */ access_token?: string; /** - * Expiration time of `access_token` in seconds. + * Format: int64 + * @description Expiration time of `access_token` in seconds. */ expires_in?: string; - /** - * The granted ID token - */ + /** @description The granted ID token */ id_token?: string; /** - * The client's project number + * Format: int64 + * @description The client's project number */ project_id?: string; - /** - * The granted refresh token; might be the same as `refreshToken` in the request. - */ + /** @description The granted refresh token; might be the same as `refreshToken` in the request. */ refresh_token?: string; - /** - * The type of `access_token`. Included to conform with the OAuth 2.0 specification; always `Bearer`. - */ + /** @description The type of `access_token`. Included to conform with the OAuth 2.0 specification; always `Bearer`. */ token_type?: string; - /** - * The local user ID - */ + /** @description The local user ID */ user_id?: string; }; - /** - * Emulator-specific configuration. - */ + /** @description Emulator-specific configuration. */ EmulatorV1ProjectsConfig: { - signIn?: { allowDuplicateEmails?: boolean }; - usageMode?: "USAGE_MODE_UNSPECIFIED" | "DEFAULT" | "PASSTHROUGH"; + signIn?: { + allowDuplicateEmails?: boolean; + }; + emailPrivacyConfig?: { + enableImprovedEmailPrivacy?: boolean, + }, }; - /** - * Details of all pending confirmation codes. - */ + /** @description Details of all pending confirmation codes. */ EmulatorV1ProjectsOobCodes: { - oobCodes?: { email?: string; oobCode?: string; oobLink?: string; requestType?: string }[]; - }; - /** - * Details of all pending verification codes. - */ + oobCodes?: { + email?: string; + oobCode?: string; + oobLink?: string; + requestType?: string; + }[]; + }; + /** @description Details of all pending verification codes. */ EmulatorV1ProjectsVerificationCodes: { - verificationCodes?: { code?: string; phoneNumber?: string; sessionInfo?: string }[]; + verificationCodes?: { + code?: string; + phoneNumber?: string; + sessionInfo?: string; + }[]; + }; + }; + parameters: { + /** @description OAuth access token. */ + access_token: string; + /** @description Data format for response. */ + alt: "json" | "media" | "proto"; + /** @description JSONP */ + callback: string; + /** @description Selector specifying which fields to include in a partial response. */ + fields: string; + /** @description OAuth 2.0 token for the current user. */ + oauth_token: string; + /** @description Returns response with indentations and line breaks. */ + prettyPrint: boolean; + /** @description Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser: string; + /** @description Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType: string; + /** @description Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol: string; + }; + requestBodies: { + GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1SignUpRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SignUpRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1DeleteAccountRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1DeleteAccountRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1UploadAccountRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1UploadAccountRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1GetAccountInfoRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1GetAccountInfoRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1GetOobCodeRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1GetOobCodeRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1SetAccountInfoRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"]; + }; + }; + GoogleCloudIdentitytoolkitV1QueryUserInfoRequest: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"]; + }; + }; + GoogleCloudIdentitytoolkitAdminV2Tenant: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Tenant"]; + }; + }; + GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + }; + GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + }; + GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; }; }; } + +export interface operations { + /** If an email identifier is specified, checks and returns if any user account is registered with the email. If there is a registered account, fetches all providers associated with the account's email. If the provider ID of an Identity Provider (IdP) is specified, creates an authorization URI for the IdP. The user can be directed to this URI to sign in with the IdP. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.createAuthUri": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1CreateAuthUriResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1CreateAuthUriRequest"]; + }; + }; + }; + /** Deletes a user's account. */ + "identitytoolkit.accounts.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1DeleteAccountResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1DeleteAccountRequest"]; + }; + /** Experimental */ + "identitytoolkit.accounts.issueSamlResponse": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1IssueSamlResponseResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1IssueSamlResponseRequest"]; + }; + }; + }; + /** Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria. */ + "identitytoolkit.accounts.lookup": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetAccountInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1GetAccountInfoRequest"]; + }; + /** Resets the password of an account either using an out-of-band code generated by sendOobCode or by specifying the email and password of the account to be modified. Can also check the purpose of an out-of-band code without consuming it. */ + "identitytoolkit.accounts.resetPassword": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1ResetPasswordResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1ResetPasswordRequest"]; + }; + }; + }; + /** Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it. */ + "identitytoolkit.accounts.sendOobCode": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetOobCodeResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1GetOobCodeRequest"]; + }; + /** Sends a SMS verification code for phone number sign-in. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.sendVerificationCode": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SendVerificationCodeResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SendVerificationCodeRequest"]; + }; + }; + }; + /** Signs in or signs up a user by exchanging a custom Auth token. Upon a successful sign-in or sign-up, a new Identity Platform ID token and refresh token are issued for the user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.signInWithCustomToken": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithCustomTokenResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithCustomTokenRequest"]; + }; + }; + }; + /** Signs in or signs up a user with a out-of-band code from an email link. If a user does not exist with the given email address, a user record will be created. If the sign-in succeeds, an Identity Platform ID and refresh token are issued for the authenticated user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.signInWithEmailLink": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithEmailLinkResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithEmailLinkRequest"]; + }; + }; + }; + /** Signs in or signs up a user with iOS Game Center credentials. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. The bundle ID is required in the request header as `x-ios-bundle-identifier`. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. Apple has [deprecated the `playerID` field](https://developer.apple.com/documentation/gamekit/gkplayer/1521127-playerid/). The Apple platform Firebase SDK will use `gamePlayerID` and `teamPlayerID` from version 10.5.0 and onwards. Upgrading to SDK version 10.5.0 or later updates existing integrations that use `playerID` to instead use `gamePlayerID` and `teamPlayerID`. When making calls to `signInWithGameCenter`, you must include `playerID` along with the new fields `gamePlayerID` and `teamPlayerID` to successfully identify all existing users. Upgrading existing Game Center sign in integrations to SDK version 10.5.0 or later is irreversible. */ + "identitytoolkit.accounts.signInWithGameCenter": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithGameCenterResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithGameCenterRequest"]; + }; + }; + }; + /** Signs in or signs up a user using credentials from an Identity Provider (IdP). This is done by manually providing an IdP credential, or by providing the authorization response obtained via the authorization request from CreateAuthUri. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. A new Identity Platform user account will be created if the user has not previously signed in to the IdP with the same account. In addition, when the "One account per email address" setting is enabled, there should not be an existing Identity Platform user account with the same email address for a new user account to be created. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.signInWithIdp": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithIdpResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithIdpRequest"]; + }; + }; + }; + /** Signs in a user with email and password. If the sign-in succeeds, a new Identity Platform ID token and refresh token are issued for the authenticated user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.signInWithPassword": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithPasswordResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithPasswordRequest"]; + }; + }; + }; + /** Completes a phone number authentication attempt. If a user already exists with the given phone number, an ID token is minted for that user. Otherwise, a new user is created and associated with the phone number. This method may also be used to link a phone number to an existing user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.signInWithPhoneNumber": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithPhoneNumberResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1SignInWithPhoneNumberRequest"]; + }; + }; + }; + /** Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.signUp": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignUpResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1SignUpRequest"]; + }; + /** Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported. */ + "identitytoolkit.accounts.update": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"]; + }; + /** Verifies an iOS client is a real iOS device. If the request is valid, a receipt will be sent in the response and a secret will be sent via Apple Push Notification Service. The client should send both of them back to certain Identity Platform APIs in a later call (for example, /accounts:sendVerificationCode), in order to verify the client. The bundle ID is required in the request header as `x-ios-bundle-identifier`. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.verifyIosClient": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1VerifyIosClientResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV1VerifyIosClientRequest"]; + }; + }; + }; + /** Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.projects.accounts": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The project ID of the project which the user should belong to. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). If this is not set, the target project is inferred from the scope associated to the Bearer access token. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignUpResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1SignUpRequest"]; + }; + /** Creates a session cookie for the given Identity Platform ID token. The session cookie is used by the client to preserve the user's login state. */ + "identitytoolkit.projects.createSessionCookie": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the project that the account belongs to. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1CreateSessionCookieResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest"]; + }; + /** Looks up user accounts within a project or a tenant based on conditions in the request. */ + "identitytoolkit.projects.queryAccounts": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the project to which the result is scoped. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1QueryUserInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"]; + }; + /** Uploads multiple accounts into the Google Cloud project. If there is a problem uploading one or more of the accounts, the rest will be uploaded, and a list of the errors will be returned. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + "identitytoolkit.projects.accounts.batchCreate": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The Project ID of the Identity Platform project which the account belongs to. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1UploadAccountResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1UploadAccountRequest"]; + }; + /** Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ + "identitytoolkit.projects.accounts.batchDelete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** If `tenant_id` is specified, the ID of the Google Cloud project that the Identity Platform tenant belongs to. Otherwise, the ID of the Google Cloud project that accounts belong to. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest"]; + }; + /** Download account information for all accounts on the project in a paginated manner. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).. Furthermore, additional permissions are needed to get password hash, password salt, and password version from accounts; otherwise these fields are redacted. */ + "identitytoolkit.projects.accounts.batchGet": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + delegatedProjectNumber?: string; + /** The maximum number of results to return. Must be at least 1 and no greater than 1000. By default, it is 20. */ + maxResults?: number; + /** The pagination token from the response of a previous request. */ + nextPageToken?: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId?: string; + }; + path: { + /** If `tenant_id` is specified, the ID of the Google Cloud project that the Identity Platform tenant belongs to. Otherwise, the ID of the Google Cloud project that the accounts belong to. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1DownloadAccountResponse"]; + }; + }; + }; + }; + /** Deletes a user's account. */ + "identitytoolkit.projects.accounts.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the project which the account belongs to. Should only be specified in authenticated requests that specify local_id of an account. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1DeleteAccountResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1DeleteAccountRequest"]; + }; + /** Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria. */ + "identitytoolkit.projects.accounts.lookup": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the Google Cloud project that the account or the Identity Platform tenant specified by `tenant_id` belongs to. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetAccountInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1GetAccountInfoRequest"]; + }; + /** Looks up user accounts within a project or a tenant based on conditions in the request. */ + "identitytoolkit.projects.accounts.query": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the project to which the result is scoped. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1QueryUserInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"]; + }; + /** Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it. */ + "identitytoolkit.projects.accounts.sendOobCode": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The Project ID of the Identity Platform project which the account belongs to. To specify this field, it requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetOobCodeResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1GetOobCodeRequest"]; + }; + /** Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported. */ + "identitytoolkit.projects.accounts.update": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"]; + }; + /** Signs up a new email and password user or anonymous user, or upgrades an anonymous user to email and password. For an admin request with a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control), creates a new anonymous, email and password, or phone number user. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.projects.tenants.accounts": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The project ID of the project which the user should belong to. Specifying this field requires a Google OAuth 2.0 credential with the proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). If this is not set, the target project is inferred from the scope associated to the Bearer access token. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant to create a user under. If not set, the user will be created under the default Identity Platform project. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SignUpResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1SignUpRequest"]; + }; + /** Creates a session cookie for the given Identity Platform ID token. The session cookie is used by the client to preserve the user's login state. */ + "identitytoolkit.projects.tenants.createSessionCookie": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the project that the account belongs to. */ + targetProjectId: string; + /** The tenant ID of the Identity Platform tenant the account belongs to. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1CreateSessionCookieResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1CreateSessionCookieRequest"]; + }; + /** Uploads multiple accounts into the Google Cloud project. If there is a problem uploading one or more of the accounts, the rest will be uploaded, and a list of the errors will be returned. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + "identitytoolkit.projects.tenants.accounts.batchCreate": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The Project ID of the Identity Platform project which the account belongs to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the account belongs to. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1UploadAccountResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1UploadAccountRequest"]; + }; + /** Batch deletes multiple accounts. For accounts that fail to be deleted, error info is contained in the response. The method ignores accounts that do not exist or are duplicated in the request. This method requires a Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). */ + "identitytoolkit.projects.tenants.accounts.batchDelete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** If `tenant_id` is specified, the ID of the Google Cloud project that the Identity Platform tenant belongs to. Otherwise, the ID of the Google Cloud project that accounts belong to. */ + targetProjectId: string; + /** If the accounts belong to an Identity Platform tenant, the ID of the tenant. If the accounts belong to a default Identity Platform project, the field is not needed. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest"]; + }; + /** Download account information for all accounts on the project in a paginated manner. To use this method requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control).. Furthermore, additional permissions are needed to get password hash, password salt, and password version from accounts; otherwise these fields are redacted. */ + "identitytoolkit.projects.tenants.accounts.batchGet": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + delegatedProjectNumber?: string; + /** The maximum number of results to return. Must be at least 1 and no greater than 1000. By default, it is 20. */ + maxResults?: number; + /** The pagination token from the response of a previous request. */ + nextPageToken?: string; + }; + path: { + /** If `tenant_id` is specified, the ID of the Google Cloud project that the Identity Platform tenant belongs to. Otherwise, the ID of the Google Cloud project that the accounts belong to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1DownloadAccountResponse"]; + }; + }; + }; + }; + /** Deletes a user's account. */ + "identitytoolkit.projects.tenants.accounts.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the project which the account belongs to. Should only be specified in authenticated requests that specify local_id of an account. */ + targetProjectId: string; + /** The ID of the tenant that the account belongs to, if applicable. Only require to be specified for authenticated requests bearing a Google OAuth 2.0 credential that specify local_id of an account that belongs to an Identity Platform tenant. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1DeleteAccountResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1DeleteAccountRequest"]; + }; + /** Gets account information for all matched accounts. For an end user request, retrieves the account of the end user. For an admin request with Google OAuth 2.0 credential, retrieves one or multiple account(s) with matching criteria. */ + "identitytoolkit.projects.tenants.accounts.lookup": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the Google Cloud project that the account or the Identity Platform tenant specified by `tenant_id` belongs to. Should only be specified by authenticated requests bearing a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + targetProjectId: string; + /** The ID of the tenant that the account belongs to. Should only be specified by authenticated requests from a developer. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetAccountInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1GetAccountInfoRequest"]; + }; + /** Looks up user accounts within a project or a tenant based on conditions in the request. */ + "identitytoolkit.projects.tenants.accounts.query": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The ID of the project to which the result is scoped. */ + targetProjectId: string; + /** The ID of the tenant to which the result is scoped. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1QueryUserInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"]; + }; + /** Sends an out-of-band confirmation code for an account. Requests from a authenticated request can optionally return a link including the OOB code instead of sending it. */ + "identitytoolkit.projects.tenants.accounts.sendOobCode": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The Project ID of the Identity Platform project which the account belongs to. To specify this field, it requires a Google OAuth 2.0 credential with proper [permissions](https://cloud.google.com/identity-platform/docs/access-control). */ + targetProjectId: string; + /** The tenant ID of the Identity Platform tenant the account belongs to. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetOobCodeResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1GetOobCodeRequest"]; + }; + /** Updates account-related information for the specified user by setting specific fields or applying action codes. Requests from administrators and end users are supported. */ + "identitytoolkit.projects.tenants.accounts.update": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + /** The project ID for the project that the account belongs to. Specifying this field requires Google OAuth 2.0 credential with proper [permissions] (https://cloud.google.com/identity-platform/docs/access-control). Requests from end users should pass an Identity Platform ID token instead. */ + targetProjectId: string; + /** The tenant ID of the Identity Platform tenant that the account belongs to. Requests from end users should pass an Identity Platform ID token rather than setting this field. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"]; + }; + /** Gets a project's public Identity Toolkit configuration. (Legacy) This method also supports authenticated calls from a developer to retrieve non-public configuration. */ + "identitytoolkit.getProjects": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** Android package name to check against the real android package name. If this field is provided, and sha1_cert_hash is not provided, the action will throw an error if this does not match the real android package name. */ + androidPackageName?: string; + /** The RP OAuth client ID. If set, a check will be performed to ensure that the OAuth client is valid for the retrieved project and the request rejected with a client error if not valid. */ + clientId?: string; + /** Project Number of the delegated project request. This field should only be used as part of the Firebase V1 migration. */ + delegatedProjectNumber?: string; + /** The Firebase app ID, for applications that use Firebase. This can be found in the Firebase console for your project. If set, a check will be performed to ensure that the app ID is valid for the retrieved project. If not valid, the request will be rejected with a client error. */ + firebaseAppId?: string; + /** iOS bundle id to check against the real ios bundle id. If this field is provided, the action will throw an error if this does not match the real iOS bundle id. */ + iosBundleId?: string; + /** Project number of the configuration to retrieve. This field is deprecated and should not be used by new integrations. */ + projectNumber?: string; + /** Whether dynamic link should be returned. */ + returnDynamicLink?: boolean; + /** SHA-1 Android application cert hash. If set, a check will be performed to ensure that the cert hash is valid for the retrieved project and android_package_name. */ + sha1Cert?: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetProjectConfigResponse"]; + }; + }; + }; + }; + /** Gets parameters needed for generating a reCAPTCHA challenge. */ + "identitytoolkit.getRecaptchaParams": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetRecaptchaParamResponse"]; + }; + }; + }; + }; + /** Retrieves the set of public keys of the session cookie JSON Web Token (JWT) signer that can be used to validate the session cookie created through createSessionCookie. */ + "identitytoolkit.getSessionCookiePublicKeys": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV1GetSessionCookiePublicKeysResponse"]; + }; + }; + }; + }; + /** Revokes a user's token from an Identity Provider (IdP). This is done by manually providing an IdP credential, and the token types for revocation. An [API key](https://cloud.google.com/docs/authentication/api-keys) is required in the request in order to identify the Google Cloud project. */ + "identitytoolkit.accounts.revokeToken": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2RevokeTokenResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV2RevokeTokenRequest"]; + }; + }; + }; + /** Finishes enrolling a second factor for the user. */ + "identitytoolkit.accounts.mfaEnrollment.finalize": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentRequest"]; + }; + }; + }; + /** Step one of the MFA enrollment process. In SMS case, this sends an SMS verification code to the user. */ + "identitytoolkit.accounts.mfaEnrollment.start": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaEnrollmentResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaEnrollmentRequest"]; + }; + }; + }; + /** Revokes one second factor from the enrolled second factors for an account. */ + "identitytoolkit.accounts.mfaEnrollment.withdraw": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2WithdrawMfaResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV2WithdrawMfaRequest"]; + }; + }; + }; + /** Verifies the MFA challenge and performs sign-in */ + "identitytoolkit.accounts.mfaSignIn.finalize": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaSignInResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV2FinalizeMfaSignInRequest"]; + }; + }; + }; + /** Sends the MFA challenge */ + "identitytoolkit.accounts.mfaSignIn.start": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaSignInResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitV2StartMfaSignInRequest"]; + }; + }; + }; + /** List all default supported Idps. */ + "identitytoolkit.defaultSupportedIdps.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of items to return. */ + pageSize?: number; + /** The next_page_token value returned from a previous List request, if any. */ + pageToken?: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListDefaultSupportedIdpsResponse"]; + }; + }; + }; + }; + /** Retrieve an Identity Toolkit project configuration. */ + "identitytoolkit.projects.getConfig": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Config"]; + }; + }; + }; + }; + /** Update an Identity Toolkit project configuration. */ + "identitytoolkit.projects.updateConfig": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The update mask applies to the resource. Fields set in the config but not included in this update mask will be ignored. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Config"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Config"]; + }; + }; + }; + /** List all default supported Idp configurations for an Identity Toolkit project. */ + "identitytoolkit.projects.defaultSupportedIdpConfigs.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of items to return. */ + pageSize?: number; + /** The next_page_token value returned from a previous List request, if any. */ + pageToken?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListDefaultSupportedIdpConfigsResponse"]; + }; + }; + }; + }; + /** Create a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.defaultSupportedIdpConfigs.create": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The id of the Idp to create a config for. Call ListDefaultSupportedIdps for list of all default supported Idps. */ + idpId?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + /** Retrieve a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.defaultSupportedIdpConfigs.get": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + defaultSupportedIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + }; + }; + }; + /** Delete a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.defaultSupportedIdpConfigs.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + defaultSupportedIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleProtobufEmpty"]; + }; + }; + }; + }; + /** Update a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.defaultSupportedIdpConfigs.patch": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The update mask applies to the resource. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + defaultSupportedIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + /** Initialize Identity Platform for a Cloud project. Identity Platform is an end-to-end authentication system for third-party users to access your apps and services. These could include mobile/web apps, games, APIs and beyond. This is the publicly available variant of EnableIdentityPlatform that is only available to billing-enabled projects. */ + "identitytoolkit.projects.identityPlatform.initializeAuth": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InitializeIdentityPlatformRequest"]; + }; + }; + }; + /** List all inbound SAML configurations for an Identity Toolkit project. */ + "identitytoolkit.projects.inboundSamlConfigs.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of items to return. */ + pageSize?: number; + /** The next_page_token value returned from a previous List request, if any. */ + pageToken?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListInboundSamlConfigsResponse"]; + }; + }; + }; + }; + /** Create an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.inboundSamlConfigs.create": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The id to use for this config. */ + inboundSamlConfigId?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + /** Retrieve an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.inboundSamlConfigs.get": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + inboundSamlConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + }; + }; + }; + /** Delete an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.inboundSamlConfigs.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + inboundSamlConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleProtobufEmpty"]; + }; + }; + }; + }; + /** Update an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.inboundSamlConfigs.patch": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The update mask applies to the resource. Empty update mask will result in updating nothing. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + inboundSamlConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + /** List all Oidc Idp configurations for an Identity Toolkit project. */ + "identitytoolkit.projects.oauthIdpConfigs.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of items to return. */ + pageSize?: number; + /** The next_page_token value returned from a previous List request, if any. */ + pageToken?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListOAuthIdpConfigsResponse"]; + }; + }; + }; + }; + /** Create an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.oauthIdpConfigs.create": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The id to use for this config. */ + oauthIdpConfigId?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + /** Retrieve an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.oauthIdpConfigs.get": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + oauthIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + }; + }; + }; + /** Delete an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.oauthIdpConfigs.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + oauthIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleProtobufEmpty"]; + }; + }; + }; + }; + /** Update an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.oauthIdpConfigs.patch": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The update mask applies to the resource. Empty update mask will result in updating nothing. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + oauthIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + /** List tenants under the given agent project. Requires read permission on the Agent project. */ + "identitytoolkit.projects.tenants.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of results to return, capped at 1000. If not specified, the default value is 20. */ + pageSize?: number; + /** The pagination token from the response of a previous request. */ + pageToken?: string; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListTenantsResponse"]; + }; + }; + }; + }; + /** Create a tenant. Requires write permission on the Agent project. */ + "identitytoolkit.projects.tenants.create": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Tenant"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2Tenant"]; + }; + /** Get a tenant. Requires read permission on the Tenant resource. */ + "identitytoolkit.projects.tenants.get": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Tenant"]; + }; + }; + }; + }; + /** Delete a tenant. Requires write permission on the Agent project. */ + "identitytoolkit.projects.tenants.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleProtobufEmpty"]; + }; + }; + }; + }; + /** Update a tenant. Requires write permission on the Tenant resource. */ + "identitytoolkit.projects.tenants.patch": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** If provided, only update fields set in the update mask. Otherwise, all settable fields will be updated. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Tenant"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2Tenant"]; + }; + /** Gets the access control policy for a resource. An error is returned if the resource does not exist. An empty policy is returned if the resource exists but does not have a policy set on it. Caller must have the right Google IAM permission on the resource. */ + "identitytoolkit.projects.tenants.getIamPolicy": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleIamV1Policy"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleIamV1GetIamPolicyRequest"]; + }; + }; + }; + /** Sets the access control policy for a resource. If the policy exists, it is replaced. Caller must have the right Google IAM permission on the resource. */ + "identitytoolkit.projects.tenants.setIamPolicy": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleIamV1Policy"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleIamV1SetIamPolicyRequest"]; + }; + }; + }; + /** Returns the caller's permissions on a resource. An error is returned if the resource does not exist. A caller is not required to have Google IAM permission to make this request. */ + "identitytoolkit.projects.tenants.testIamPermissions": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleIamV1TestIamPermissionsResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GoogleIamV1TestIamPermissionsRequest"]; + }; + }; + }; + /** List all default supported Idp configurations for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of items to return. */ + pageSize?: number; + /** The next_page_token value returned from a previous List request, if any. */ + pageToken?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListDefaultSupportedIdpConfigsResponse"]; + }; + }; + }; + }; + /** Create a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.create": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The id of the Idp to create a config for. Call ListDefaultSupportedIdps for list of all default supported Idps. */ + idpId?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + /** Retrieve a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.get": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + defaultSupportedIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + }; + }; + }; + /** Delete a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + defaultSupportedIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleProtobufEmpty"]; + }; + }; + }; + }; + /** Update a default supported Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.defaultSupportedIdpConfigs.patch": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The update mask applies to the resource. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + defaultSupportedIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdpConfig"]; + }; + /** List all inbound SAML configurations for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.inboundSamlConfigs.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of items to return. */ + pageSize?: number; + /** The next_page_token value returned from a previous List request, if any. */ + pageToken?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListInboundSamlConfigsResponse"]; + }; + }; + }; + }; + /** Create an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.inboundSamlConfigs.create": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The id to use for this config. */ + inboundSamlConfigId?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + /** Retrieve an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.inboundSamlConfigs.get": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + inboundSamlConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + }; + }; + }; + /** Delete an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.inboundSamlConfigs.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + inboundSamlConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleProtobufEmpty"]; + }; + }; + }; + }; + /** Update an inbound SAML configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.inboundSamlConfigs.patch": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The update mask applies to the resource. Empty update mask will result in updating nothing. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + inboundSamlConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2InboundSamlConfig"]; + }; + /** List all Oidc Idp configurations for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.oauthIdpConfigs.list": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The maximum number of items to return. */ + pageSize?: number; + /** The next_page_token value returned from a previous List request, if any. */ + pageToken?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ListOAuthIdpConfigsResponse"]; + }; + }; + }; + }; + /** Create an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.oauthIdpConfigs.create": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The id to use for this config. */ + oauthIdpConfigId?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + /** Retrieve an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.oauthIdpConfigs.get": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + oauthIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + }; + }; + }; + /** Delete an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.oauthIdpConfigs.delete": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + path: { + targetProjectId: string; + tenantId: string; + oauthIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleProtobufEmpty"]; + }; + }; + }; + }; + /** Update an Oidc Idp configuration for an Identity Toolkit project. */ + "identitytoolkit.projects.tenants.oauthIdpConfigs.patch": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The update mask applies to the resource. Empty update mask will result in updating nothing. For the `FieldMask` definition, see https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask */ + updateMask?: string; + }; + path: { + targetProjectId: string; + tenantId: string; + oauthIdpConfigsId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + }; + }; + requestBody: components["requestBodies"]["GoogleCloudIdentitytoolkitAdminV2OAuthIdpConfig"]; + }; + /** Gets password policy config set on the project or tenant. */ + "identitytoolkit.getPasswordPolicy": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** The id of a tenant. */ + tenantId?: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2PasswordPolicy"]; + }; + }; + }; + }; + /** Gets parameters needed for reCAPTCHA analysis. */ + "identitytoolkit.getRecaptchaConfig": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + /** reCAPTCHA Enterprise uses separate site keys for different client types. Specify the client type to get the corresponding key. */ + clientType?: + | "CLIENT_TYPE_UNSPECIFIED" + | "CLIENT_TYPE_WEB" + | "CLIENT_TYPE_ANDROID" + | "CLIENT_TYPE_IOS"; + /** The id of a tenant. */ + tenantId?: string; + /** The reCAPTCHA version. */ + version?: "RECAPTCHA_VERSION_UNSPECIFIED" | "RECAPTCHA_ENTERPRISE"; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GoogleCloudIdentitytoolkitV2RecaptchaConfig"]; + }; + }; + }; + }; + /** The Token Service API lets you exchange either an ID token or a refresh token for an access token and a new refresh token. You can use the access token to securely call APIs that require user authorization. */ + "securetoken.token": { + parameters: { + query: { + /** OAuth access token. */ + access_token?: components["parameters"]["access_token"]; + /** Data format for response. */ + alt?: components["parameters"]["alt"]; + /** JSONP */ + callback?: components["parameters"]["callback"]; + /** Selector specifying which fields to include in a partial response. */ + fields?: components["parameters"]["fields"]; + /** OAuth 2.0 token for the current user. */ + oauth_token?: components["parameters"]["oauth_token"]; + /** Returns response with indentations and line breaks. */ + prettyPrint?: components["parameters"]["prettyPrint"]; + /** Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. */ + quotaUser?: components["parameters"]["quotaUser"]; + /** Legacy upload protocol for media (e.g. "media", "multipart"). */ + uploadType?: components["parameters"]["uploadType"]; + /** Upload protocol for media (e.g. "raw", "multipart"). */ + upload_protocol?: components["parameters"]["upload_protocol"]; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "*/*": components["schemas"]["GrantTokenResponse"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GrantTokenRequest"]; + "application/x-www-form-urlencoded": components["schemas"]["GrantTokenRequest"]; + }; + }; + }; + /** Remove all accounts in the project, regardless of state. */ + "emulator.projects.accounts.delete": { + parameters: { + path: { + /** The ID of the Google Cloud project that the accounts belong to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "application/json": { [key: string]: unknown }; + }; + }; + }; + }; + /** Get emulator-specific configuration for the project. */ + "emulator.projects.config.get": { + parameters: { + path: { + /** The ID of the Google Cloud project that the config belongs to. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "application/json": components["schemas"]["EmulatorV1ProjectsConfig"]; + }; + }; + }; + }; + /** Update emulator-specific configuration for the project. */ + "emulator.projects.config.update": { + parameters: { + path: { + /** The ID of the Google Cloud project that the config belongs to. */ + targetProjectId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "application/json": components["schemas"]["EmulatorV1ProjectsConfig"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EmulatorV1ProjectsConfig"]; + }; + }; + }; + /** List all pending confirmation codes for the project. */ + "emulator.projects.oobCodes.list": { + parameters: { + path: { + /** The ID of the Google Cloud project that the confirmation codes belongs to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "application/json": components["schemas"]["EmulatorV1ProjectsOobCodes"]; + }; + }; + }; + }; + /** List all pending phone verification codes for the project. */ + "emulator.projects.verificationCodes.list": { + parameters: { + path: { + /** The ID of the Google Cloud project that the verification codes belongs to. */ + targetProjectId: string; + /** The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned. */ + tenantId: string; + }; + }; + responses: { + /** Successful response */ + 200: { + content: { + "application/json": components["schemas"]["EmulatorV1ProjectsOobCodes"]; + }; + }; + }; + }; +} + +export interface external {} diff --git a/src/emulator/auth/server.ts b/src/emulator/auth/server.ts index e3d526ce629..c8229e85e2b 100644 --- a/src/emulator/auth/server.ts +++ b/src/emulator/auth/server.ts @@ -3,11 +3,12 @@ import * as express from "express"; import * as exegesisExpress from "exegesis-express"; import { ValidationError } from "exegesis/lib/errors"; import * as _ from "lodash"; +import { SingleProjectMode } from "./index"; import { OpenAPIObject, PathsObject, ServerObject, OperationObject } from "openapi3-ts"; import { EmulatorLogger } from "../emulatorLogger"; import { Emulators } from "../types"; import { authOperations, AuthOps, AuthOperation, FirebaseJwtPayload } from "./operations"; -import { AgentProjectState, ProjectState } from "./state"; +import { AgentProjectState, decodeRefreshToken, ProjectState } from "./state"; import apiSpecUntyped from "./apiSpec"; import { PromiseController, @@ -31,7 +32,7 @@ import { import { logError } from "./utils"; import { camelCase } from "lodash"; import { registerHandlers } from "./handlers"; -import bodyParser = require("body-parser"); +import * as bodyParser from "body-parser"; import { URLSearchParams } from "url"; import { decode, JwtHeader } from "jsonwebtoken"; const apiSpec = apiSpecUntyped as OpenAPIObject; @@ -116,10 +117,23 @@ function specWithEmulatorServer(protocol: string, host: string | undefined): Ope */ export async function createApp( defaultProjectId: string, - projectStateForId = new Map() + singleProjectMode = SingleProjectMode.NO_WARNING, + projectStateForId = new Map(), ): Promise { const app = express(); app.set("json spaces", 2); + + // Return access-control-allow-private-network heder if requested + // Enables accessing locahost when site is exposed via tunnel see https://github.com/firebase/firebase-tools/issues/4227 + // Aligns with https://wicg.github.io/private-network-access/#headers + // Replace with cors option if adopted, see https://github.com/expressjs/cors/issues/236 + app.use("/", (req, res, next) => { + if (req.headers["access-control-request-private-network"]) { + res.setHeader("access-control-allow-private-network", "true"); + } + next(); + }); + // Enable CORS for all APIs, all origins (reflected), and all headers (reflected). // This is similar to production behavior. Safe since all APIs are cookieless. app.use(cors({ origin: true })); @@ -148,18 +162,29 @@ export async function createApp( registerLegacyRoutes(app); registerHandlers(app, (apiKey, tenantId) => - getProjectStateById(getProjectIdByApiKey(apiKey), tenantId) + getProjectStateById(getProjectIdByApiKey(apiKey), tenantId), ); const apiKeyAuthenticator: PromiseAuthenticator = (ctx, info) => { - if (info.in !== "query") { - throw new Error('apiKey must be defined as in: "query" in API spec.'); - } if (!info.name) { - throw new Error("apiKey param name is undefined in API spec."); + throw new Error("apiKey param/header name is undefined in API spec."); } - const key = (ctx.req as express.Request).query[info.name]; - if (typeof key === "string" && key.length > 0) { + + let key: string | undefined; + const req = ctx.req as express.Request; + switch (info.in) { + case "header": + key = req.get(info.name); + break; + case "query": { + const q = req.query[info.name]; + key = typeof q === "string" ? q : undefined; + break; + } + default: + throw new Error('apiKey must be defined as in: "query" or "header" in API spec.'); + } + if (key) { return { type: "success", user: getProjectIdByApiKey(key) }; } else { return undefined; @@ -172,7 +197,7 @@ export async function createApp( return undefined; } const scopes = Object.keys( - ctx.api.openApiDoc.components.securitySchemes.Oauth2.flows.authorizationCode.scopes + ctx.api.openApiDoc.components.securitySchemes.Oauth2.flows.authorizationCode.scopes, ); const token = authorization.substr(AUTH_HEADER_PREFIX.length); if (token.toLowerCase() === "owner") { @@ -184,7 +209,7 @@ export async function createApp( // will also assume that the token belongs to the default projectId. EmulatorLogger.forEmulator(Emulators.AUTH).log( "WARN", - `Received service account token ${token}. Assuming that it owns project "${defaultProjectId}".` + `Received service account token ${token}. Assuming that it owns project "${defaultProjectId}".`, ); return { type: "success", user: defaultProjectId, scopes }; } @@ -198,13 +223,14 @@ export async function createApp( location: "Authorization", locationType: "header", }, - ] + ], ); }; const apis = await exegesisExpress.middleware(specForRouter(), { controllers: { auth: toExegesisController(authOperations, getProjectStateById) }, authenticators: { - apiKey: apiKeyAuthenticator, + apiKeyQuery: apiKeyAuthenticator, + apiKeyHeader: apiKeyAuthenticator, Oauth2: oauth2Authenticator, }, autoHandleHttpErrors(err) { @@ -242,12 +268,12 @@ export async function createApp( onResponseValidationError({ errors }) { logError( new Error( - `An internal error occured when generating response. Details:\n${JSON.stringify(errors)}` - ) + `An internal error occured when generating response. Details:\n${JSON.stringify(errors)}`, + ), ); throw new InternalError( "An internal error occured when generating response.", - "emulator-response-validation" + "emulator-response-validation", ); }, customFormats: { @@ -271,6 +297,12 @@ export async function createApp( // TODO return true; }, + byte() { + // Disable the "byte" format validation to allow stuffing arbitrary + // strings in passwordHash etc. Needed because the emulator generates + // non-base64 hash strings like "fakeHash:salt=foo:password=bar". + return true; + }, }, plugins: [ { @@ -285,7 +317,7 @@ export async function createApp( if (ctx.res.statusCode === 401) { // Normalize unauthenticated responses to match production. const requirements = (ctx.api.operationObject as OperationObject).security; - if (requirements?.some((req) => req.apiKey)) { + if (requirements?.some((req) => req.apiKeyQuery || req.apiKeyHeader)) { throw new PermissionDeniedError("The request is missing a valid API key."); } else { throw new UnauthenticatedError( @@ -298,7 +330,7 @@ export async function createApp( location: "Authorization", locationType: "header", }, - ] + ], ); } } @@ -344,6 +376,22 @@ export async function createApp( function getProjectStateById(projectId: string, tenantId?: string): ProjectState { let agentState = projectStateForId.get(projectId); + + if ( + singleProjectMode !== SingleProjectMode.NO_WARNING && + projectId && + defaultProjectId !== projectId + ) { + const errorString = + `Multiple projectIds are not recommended in single project mode. ` + + `Requested project ID ${projectId}, but the emulator is configured for ` + + `${defaultProjectId}. To opt-out of single project mode add/set the ` + + `\'"singleProjectMode"\' false' property in the firebase.json emulators config.`; + EmulatorLogger.forEmulator(Emulators.AUTH).log("WARN", errorString); + if (singleProjectMode === SingleProjectMode.ERROR) { + throw new BadRequestError(errorString); + } + } if (!agentState) { agentState = new AgentProjectState(projectId); projectStateForId.set(projectId, agentState); @@ -426,7 +474,7 @@ function registerLegacyRoutes(app: express.Express): void { function toExegesisController( ops: AuthOps, - getProjectStateById: (projectId: string, tenantId?: string) => ProjectState + getProjectStateById: (projectId: string, tenantId?: string) => ProjectState, ): Record { const result: Record = {}; processNested(ops, ""); @@ -470,7 +518,7 @@ function toExegesisController( // authenticated requests may specify targetProjectId. assert( ctx.security?.Oauth2, - "INSUFFICIENT_PERMISSION : Only authenticated requests can specify target_project_id." + "INSUFFICIENT_PERMISSION : Only authenticated requests can specify target_project_id.", ); } } else { @@ -490,7 +538,7 @@ function toExegesisController( // Perform initial token parsing to get correct project state if (ctx.requestBody?.idToken) { const idToken = ctx.requestBody?.idToken; - const decoded = decode(idToken, { complete: true }) as { + const decoded = decode(idToken, { complete: true }) as any as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -500,6 +548,19 @@ function toExegesisController( targetTenantId = targetTenantId || decoded?.payload.firebase.tenant; } + // Need to check refresh token for tenant ID for grantToken endpoint + if (ctx.requestBody?.refreshToken) { + const refreshTokenRecord = decodeRefreshToken(ctx.requestBody!.refreshToken); + if (refreshTokenRecord.tenantId && targetTenantId) { + // Shouldn't ever reach this assertion, but adding for completeness + assert( + refreshTokenRecord.tenantId === targetTenantId, + "TENANT_ID_MISMATCH: ((Refresh token tenant ID does not match target tenant ID.))", + ); + } + targetTenantId = targetTenantId || refreshTokenRecord.tenantId; + } + return operation(getProjectStateById(targetProjectId, targetTenantId), ctx.requestBody, ctx); }; } @@ -507,16 +568,18 @@ function toExegesisController( function wrapValidateBody(pluginContext: ExegesisPluginContext): void { // Apply fixes to body for Google REST API mapping compatibility. - const op = ((pluginContext as unknown) as { - _operation: { - validateBody?: ValidatorFunction; - _authEmulatorValidateBodyWrapped?: true; - }; - })._operation; + const op = ( + pluginContext as unknown as { + _operation: { + validateBody?: ValidatorFunction; + _authEmulatorValidateBodyWrapped?: true; + }; + } + )._operation; if (op.validateBody && !op._authEmulatorValidateBodyWrapped) { const validateBody = op.validateBody.bind(op); op.validateBody = (body) => { - return validateAndFixRestMappingRequestBody(validateBody, body, pluginContext.api); + return validateAndFixRestMappingRequestBody(validateBody, body); }; op._authEmulatorValidateBodyWrapped = true; } @@ -526,8 +589,6 @@ function validateAndFixRestMappingRequestBody( validate: ValidatorFunction, // eslint-disable-next-line @typescript-eslint/no-explicit-any body: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - api: any ): ReturnType { body = convertKeysToCamelCase(body); diff --git a/src/test/emulators/auth/setAccountInfo.spec.ts b/src/emulator/auth/setAccountInfo.spec.ts similarity index 96% rename from src/test/emulators/auth/setAccountInfo.spec.ts rename to src/emulator/auth/setAccountInfo.spec.ts index 876056a4806..09496f0adaf 100644 --- a/src/test/emulators/auth/setAccountInfo.spec.ts +++ b/src/emulator/auth/setAccountInfo.spec.ts @@ -1,8 +1,8 @@ import { expect } from "chai"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { ProviderUserInfo, PROVIDER_PASSWORD, PROVIDER_PHONE } from "../../../emulator/auth/state"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; +import { FirebaseJwtPayload } from "./operations"; +import { ProviderUserInfo, PROVIDER_PASSWORD, PROVIDER_PHONE } from "./state"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; import { expectStatusCode, getAccountInfoByIdToken, @@ -20,10 +20,8 @@ import { TEST_PHONE_NUMBER_2, TEST_PHONE_NUMBER_3, TEST_INVALID_PHONE_NUMBER, - deleteAccount, - updateProjectConfig, registerTenant, -} from "./helpers"; +} from "./testing/helpers"; describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { it("should allow updating and deleting displayName and photoUrl", async () => { @@ -69,7 +67,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { // Updating password causes new tokens to be issued. expect(res.body).to.have.property("refreshToken").that.is.a("string"); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -104,7 +102,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { // Adding password causes new tokens to be issued. expect(res.body).to.have.property("refreshToken").that.is.a("string"); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -140,7 +138,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { // Setting email causes new tokens to be issued. expect(res.body).to.have.property("refreshToken").that.is.a("string"); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -169,7 +167,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { // Changing email causes new tokens to be issued. expect(res.body).to.have.property("refreshToken").that.is.a("string"); const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -552,7 +550,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { const { localId, idToken } = await registerUser(authApi(), user); const savedUserInfo = await getAccountInfoByIdToken(authApi(), idToken); expect(savedUserInfo.mfaInfo).to.have.length(2); - const oldEnrollmentIds = savedUserInfo.mfaInfo!.map((_) => _.mfaEnrollmentId); + const oldEnrollmentIds = savedUserInfo.mfaInfo!.map((info) => info.mfaEnrollmentId); const newMfaInfo = { displayName: "New New", @@ -645,7 +643,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { .then((res) => { expectStatusCode(400, res); expect(res.body.error.message).to.eq( - "INVALID_MFA_ENROLLMENT_ID : mfaEnrollmentId must be defined." + "INVALID_MFA_ENROLLMENT_ID : mfaEnrollmentId must be defined.", ); }); }); @@ -919,7 +917,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { .then((res) => { expectStatusCode(400, res); expect(res.body.error.message).to.eq( - "Invalid JSON payload received. /mfa/enrollments should be array" + "Invalid JSON payload received. /mfa/enrollments must be array", ); }); }); @@ -989,7 +987,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { function itShouldDeleteProvider( createUser: () => Promise<{ idToken: string; email?: string }>, - providerId: string + providerId: string, ): void { it(`should delete ${providerId} provider from user`, async () => { const user = await createUser(); @@ -1000,7 +998,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { .then((res) => { expectStatusCode(200, res); const providers = (res.body.providerUserInfo || []).map( - (info: ProviderUserInfo) => info.providerId + (info: ProviderUserInfo) => info.providerId, ); expect(providers).not.to.include(providerId); }); @@ -1014,7 +1012,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { itShouldDeleteProvider( () => registerUser(authApi(), { email: "alice@example.com", password: "notasecret" }), - PROVIDER_PASSWORD + PROVIDER_PASSWORD, ); itShouldDeleteProvider(() => signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER), PROVIDER_PHONE); itShouldDeleteProvider( @@ -1023,7 +1021,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { sub: "12345", email: "bob@example.com", }), - "google.com" + "google.com", ); it("should update user by localId when authenticated", async () => { @@ -1077,7 +1075,7 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { .then((res) => expectStatusCode(200, res)); const { idToken } = await signInWithEmailLink(authApi(), email); - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -1140,25 +1138,6 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { }); }); - it("should error if usageMode is passthrough", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { idToken } = await registerUser(authApi(), user); - const newPassword = "notasecreteither"; - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:update") - .query({ key: "fake-api-key" }) - .send({ idToken, password: newPassword }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - it("should error if auth is disabled", async () => { const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); diff --git a/src/emulator/auth/signUp.spec.ts b/src/emulator/auth/signUp.spec.ts new file mode 100644 index 00000000000..ca62e4b9079 --- /dev/null +++ b/src/emulator/auth/signUp.spec.ts @@ -0,0 +1,866 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; +import { FirebaseJwtPayload } from "./operations"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; +import { + expectStatusCode, + getAccountInfoByIdToken, + getAccountInfoByLocalId, + registerUser, + signInWithFakeClaims, + registerAnonUser, + signInWithPhoneNumber, + updateAccountByLocalId, + getSigninMethods, + TEST_MFA_INFO, + TEST_PHONE_NUMBER, + TEST_PHONE_NUMBER_2, + TEST_INVALID_PHONE_NUMBER, + registerTenant, + updateConfig, + BLOCKING_FUNCTION_HOST, + BEFORE_CREATE_PATH, + BEFORE_CREATE_URL, + BEFORE_SIGN_IN_URL, + BEFORE_SIGN_IN_PATH, + DISPLAY_NAME, + PHOTO_URL, +} from "./testing/helpers"; + +describeAuthEmulator("accounts:signUp", ({ authApi }) => { + it("should throw error if no email provided", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ password: "notasecret" /* no email */ }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("MISSING_EMAIL"); + }); + }); + + it("should throw error if empty email and password is provided", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email: "", password: "" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("INVALID_EMAIL"); + }); + }); + + it("should issue idToken and refreshToken on anon signUp", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ returnSecureToken: true }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.user_id).to.be.a("string"); + expect(decoded!.payload.provider_id).equals("anonymous"); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("anonymous"); + }); + }); + + it("should issue refreshToken on email+password signUp", async () => { + const email = "me@example.com"; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email, password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.header.alg).to.eql("none"); + expect(decoded!.payload.user_id).to.be.a("string"); + expect(decoded!.payload).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); + expect(decoded!.payload.firebase.identities).to.eql({ + email: [email], + }); + }); + }); + + it("should ignore displayName and photoUrl for new anon account", async () => { + const user = { + displayName: "Me", + photoUrl: "http://localhost/my-profile.png", + }; + const idToken = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send(user) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.displayName).to.be.undefined; + expect(res.body.photoUrl).to.be.undefined; + return res.body.idToken; + }); + const info = await getAccountInfoByIdToken(authApi(), idToken); + expect(info.displayName).to.be.undefined; + expect(info.photoUrl).to.be.undefined; + }); + + it("should set displayName but ignore photoUrl for new password account", async () => { + const user = { + email: "me@example.com", + password: "notasecret", + displayName: "Me", + photoUrl: "http://localhost/my-profile.png", + }; + const idToken = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send(user) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.displayName).to.equal(user.displayName); + expect(res.body.photoUrl).to.be.undefined; + return res.body.idToken; + }); + const info = await getAccountInfoByIdToken(authApi(), idToken); + expect(info.displayName).to.equal(user.displayName); + expect(info.photoUrl).to.be.undefined; + }); + + it("should disallow duplicate email signUp", async () => { + const user = { email: "bob@example.com", password: "notasecret" }; + await registerUser(authApi(), user); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email: user.email, password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + // Case variants of a same email address are also considered duplicates. + .send({ email: "BOB@example.com", password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); + }); + }); + + it("should error if another account exists with same email from IDP", async () => { + const email = "alice@example.com"; + await signInWithFakeClaims(authApi(), "google.com", { sub: "123", email }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email, password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); + }); + }); + + it("should error when email format is invalid", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email: "not.an.email.address.at.all", password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("INVALID_EMAIL"); + }); + }); + + it("should normalize email address to all lowercase", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email: "AlIcE@exAMPle.COM", password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).equals("alice@example.com"); + }); + }); + + it("should error when password is too short", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email: "me@example.com", password: "short" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error) + .to.have.property("message") + .that.satisfy((str: string) => str.startsWith("WEAK_PASSWORD")); + }); + }); + + it("should error when idToken is provided but email / password is not", async () => { + const { idToken } = await registerAnonUser(authApi()); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ idToken /* no email / password */ }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("MISSING_EMAIL"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ idToken, email: "alice@example.com" /* no password */ }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("MISSING_PASSWORD"); + }); + }); + + it("should link email and password to anon user if idToken is provided", async () => { + const { idToken, localId } = await registerAnonUser(authApi()); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ idToken, email: "alice@example.com", password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).to.equal(localId); + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); + }); + }); + + it("should link email and password to phone sign-in user", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + const email = "alice@example.com"; + + const { idToken, localId } = await signInWithPhoneNumber(authApi(), phoneNumber); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ idToken, email, password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).to.equal(localId); + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); + + // The result account should have both phone and email. + expect(decoded!.payload.firebase.identities).to.eql({ + phone: [phoneNumber], + email: [email], + }); + }); + }); + + it("should error if account to be linked is disabled", async () => { + const { idToken, localId } = await registerAnonUser(authApi()); + await updateAccountByLocalId(authApi(), localId, { disableUser: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ idToken, email: "alice@example.com", password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + + it("should replace existing email / password in linked account", async () => { + const oldEmail = "alice@example.com"; + const newEmail = "bob@example.com"; + const oldPassword = "notasecret"; + const newPassword = "notasecret2"; + + const { idToken, localId } = await registerUser(authApi(), { + email: oldEmail, + password: oldPassword, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ idToken, email: newEmail, password: newPassword }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).to.equal(localId); + expect(res.body.email).to.equal(newEmail); + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.email).to.equal(newEmail); + expect(decoded!.payload.firebase.identities).to.eql({ + email: [newEmail], + }); + }); + + const oldEmailSignInMethods = await getSigninMethods(authApi(), oldEmail); + expect(oldEmailSignInMethods).to.be.empty; + }); + + it("should create new account with phone number when authenticated", async () => { + const phoneNumber = TEST_PHONE_NUMBER; + const displayName = "Alice"; + const localId = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("Authorization", "Bearer owner") + .send({ phoneNumber, displayName }) + .then((res) => { + expectStatusCode(200, res); + + // Shouldn't be set for authenticated requests: + expect(res.body).not.to.have.property("idToken"); + expect(res.body).not.to.have.property("refreshToken"); + + expect(res.body.displayName).to.equal(displayName); + expect(res.body.localId).to.be.a("string").and.not.empty; + return res.body.localId as string; + }); + + // This should sign into the same user. + const phoneAuth = await signInWithPhoneNumber(authApi(), phoneNumber); + expect(phoneAuth.localId).to.equal(localId); + + const info = await getAccountInfoByIdToken(authApi(), phoneAuth.idToken); + expect(info.displayName).to.equal(displayName); // should already be set. + }); + + it("should error when extra localId parameter is provided", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({ localId: "anything" /* cannot be specified since this is unauthenticated */ }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("UNEXPECTED_PARAMETER : User ID"); + }); + + const { idToken, localId } = await registerAnonUser(authApi()); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("Authorization", "Bearer owner") + .send({ + idToken, + localId, + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("UNEXPECTED_PARAMETER : User ID"); + }); + }); + + it("should create new account with specified localId when authenticated", async () => { + const localId = "haha"; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("Authorization", "Bearer owner") + .send({ localId }) + .then((res) => { + expectStatusCode(200, res); + + // Shouldn't be set for authenticated requests: + expect(res.body).not.to.have.property("idToken"); + expect(res.body).not.to.have.property("refreshToken"); + + expect(res.body.localId).to.equal(localId); + }); + }); + + it("should error when creating new user with duplicate localId", async () => { + const { localId } = await registerAnonUser(authApi()); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("Authorization", "Bearer owner") + .send({ localId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("DUPLICATE_LOCAL_ID"); + }); + }); + + it("should error if phone number is invalid", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("Authorization", "Bearer owner") + .send({ phoneNumber: TEST_INVALID_PHONE_NUMBER }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("INVALID_PHONE_NUMBER : Invalid format."); + }); + }); + + it("should create new account with multi factor info", async () => { + const user = { email: "alice@example.com", password: "notasecret", mfaInfo: [TEST_MFA_INFO] }; + const { localId } = await registerUser(authApi(), user); + const info = await getAccountInfoByLocalId(authApi(), localId); + expect(info.mfaInfo).to.have.length(1); + const savedMfaInfo = info.mfaInfo![0]; + expect(savedMfaInfo).to.include(TEST_MFA_INFO); + expect(savedMfaInfo?.mfaEnrollmentId).to.be.a("string").and.not.empty; + }); + + it("should create new account with two MFA factors", async () => { + const user = { + email: "alice@example.com", + password: "notasecret", + mfaInfo: [TEST_MFA_INFO, { ...TEST_MFA_INFO, phoneInfo: TEST_PHONE_NUMBER_2 }], + }; + const { localId } = await registerUser(authApi(), user); + const info = await getAccountInfoByLocalId(authApi(), localId); + expect(info.mfaInfo).to.have.length(2); + for (const savedMfaInfo of info.mfaInfo!) { + if (savedMfaInfo.phoneInfo !== TEST_MFA_INFO.phoneInfo) { + expect(savedMfaInfo.phoneInfo).to.eq(TEST_PHONE_NUMBER_2); + } else { + expect(savedMfaInfo).to.include(TEST_MFA_INFO); + } + expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; + } + }); + + it("should de-duplicate factors with the same info on create", async () => { + const alice = { + email: "alice@example.com", + password: "notasecret", + mfaInfo: [TEST_MFA_INFO, TEST_MFA_INFO, TEST_MFA_INFO], + }; + const { localId: aliceLocalId } = await registerUser(authApi(), alice); + const aliceInfo = await getAccountInfoByLocalId(authApi(), aliceLocalId); + expect(aliceInfo.mfaInfo).to.have.length(1); + expect(aliceInfo.mfaInfo![0]).to.include(TEST_MFA_INFO); + expect(aliceInfo.mfaInfo![0].mfaEnrollmentId).to.be.a("string").and.not.empty; + + const bob = { + email: "bob@example.com", + password: "notasecret", + mfaInfo: [ + TEST_MFA_INFO, + TEST_MFA_INFO, + TEST_MFA_INFO, + { ...TEST_MFA_INFO, phoneInfo: TEST_PHONE_NUMBER_2 }, + ], + }; + const { localId: bobLocalId } = await registerUser(authApi(), bob); + const bobInfo = await getAccountInfoByLocalId(authApi(), bobLocalId); + expect(bobInfo.mfaInfo).to.have.length(2); + for (const savedMfaInfo of bobInfo.mfaInfo!) { + if (savedMfaInfo.phoneInfo !== TEST_MFA_INFO.phoneInfo) { + expect(savedMfaInfo.phoneInfo).to.eq(TEST_PHONE_NUMBER_2); + } else { + expect(savedMfaInfo).to.include(TEST_MFA_INFO); + } + expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; + } + }); + + it("does not require a display name for multi factor info", async () => { + const mfaInfo = { phoneInfo: TEST_PHONE_NUMBER }; + const user = { email: "alice@example.com", password: "notasecret", mfaInfo: [mfaInfo] }; + const { localId } = await registerUser(authApi(), user); + + const info = await getAccountInfoByLocalId(authApi(), localId); + expect(info.mfaInfo).to.have.length(1); + const savedMfaInfo = info.mfaInfo![0]; + expect(savedMfaInfo).to.include(mfaInfo); + expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; + expect(savedMfaInfo.displayName).to.be.undefined; + }); + + it("should error if multi factor phone number is invalid", async () => { + const mfaInfo = { phoneInfo: TEST_INVALID_PHONE_NUMBER }; + const user = { email: "alice@example.com", password: "notasecret", mfaInfo: [mfaInfo] }; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("Authorization", "Bearer owner") + .send(user) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("INVALID_MFA_PHONE_NUMBER : Invalid format."); + }); + }); + + it("should ignore if multi factor enrollment ID is specified on create", async () => { + const mfaEnrollmentId1 = "thisShouldBeIgnored1"; + const mfaEnrollmentId2 = "thisShouldBeIgnored2"; + const user = { + email: "alice@example.com", + password: "notasecret", + mfaInfo: [ + { + ...TEST_MFA_INFO, + mfaEnrollmentId: mfaEnrollmentId1, + }, + { + ...TEST_MFA_INFO, + mfaEnrollmentId: mfaEnrollmentId2, + }, + ], + }; + const { localId } = await registerUser(authApi(), user); + const info = await getAccountInfoByLocalId(authApi(), localId); + expect(info.mfaInfo).to.have.length(1); + const savedMfaInfo = info.mfaInfo![0]; + expect(savedMfaInfo).to.include(TEST_MFA_INFO); + expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; + expect([mfaEnrollmentId1, mfaEnrollmentId2]).not.to.include(savedMfaInfo.mfaEnrollmentId); + }); + + it("should error if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").includes("PROJECT_DISABLED"); + }); + }); + + it("should error if password sign up is not allowed", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { allowPasswordSignup: false }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId, email: "me@example.com", password: "notasecret" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").includes("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should error if anonymous user is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { enableAnonymousUser: false }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId, returnSecureToken: true }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").includes("ADMIN_ONLY_OPERATION"); + }); + }); + + it("should create new account with tenant info", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { allowPasswordSignup: true }); + const user = { tenantId: tenant.tenantId, email: "alice@example.com", password: "notasecret" }; + + const localId = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send(user) + .then((res) => { + expectStatusCode(200, res); + return res.body.localId; + }); + const info = await getAccountInfoByLocalId(authApi(), localId, tenant.tenantId); + + expect(info.tenantId).to.eql(tenant.tenantId); + }); + + describe("when blocking functions are present", () => { + afterEach(() => { + expect(nock.isDone()).to.be.true; + nock.cleanAll(); + }); + + it("should update modifiable fields with beforeCreate response for a new email/password user", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + }, + }); + + const email = "me@example.com"; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email, password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); + expect(decoded!.payload.firebase.identities).to.eql({ + email: [email], + }); + + expect(res.body.displayName).to.equal(DISPLAY_NAME); + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + }); + }); + + it("should update modifiable fields with beforeSignIn response for a new email/password user", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + + const email = "me@example.com"; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email, password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); + expect(decoded!.payload.firebase.identities).to.eql({ + email: [email], + }); + + expect(res.body.displayName).to.equal(DISPLAY_NAME); + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("beforeSignIn fields should overwrite beforeCreate fields", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims", + displayName: "oldDisplayName", + photoUrl: "oldPhotoUrl", + emailVerified: false, + customClaims: { customAttribute: "oldCustom" }, + }, + }) + .post(BEFORE_SIGN_IN_PATH) + .reply(200, { + userRecord: { + updateMask: "displayName,photoUrl,emailVerified,customClaims,sessionClaims", + displayName: DISPLAY_NAME, + photoUrl: PHOTO_URL, + emailVerified: true, + customClaims: { customAttribute: "custom" }, + sessionClaims: { sessionAttribute: "session" }, + }, + }); + + const email = "me@example.com"; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email, password: "notasecret" }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); + expect(decoded!.payload.firebase.identities).to.eql({ + email: [email], + }); + + expect(res.body.displayName).to.equal(DISPLAY_NAME); + expect(decoded!.payload.name).to.equal(DISPLAY_NAME); + expect(decoded!.payload.picture).to.equal(PHOTO_URL); + expect(decoded!.payload.email_verified).to.be.true; + expect(decoded!.payload).to.have.property("customAttribute").equals("custom"); + expect(decoded!.payload).to.have.property("sessionAttribute").equals("session"); + }); + }); + + it("should disable new user if set", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(200, { + userRecord: { + updateMask: "disabled", + disabled: true, + }, + }); + + const email = "me@example.com"; + const password = "notasecret"; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ email, password }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + + it("should not trigger blocking functions for privileged requests", async () => { + await updateConfig( + authApi(), + PROJECT_ID, + { + blockingFunctions: { + triggers: { + beforeCreate: { + functionUri: BEFORE_CREATE_URL, + }, + beforeSignIn: { + functionUri: BEFORE_SIGN_IN_URL, + }, + }, + }, + }, + "blockingFunctions", + ); + nock(BLOCKING_FUNCTION_HOST) + .post(BEFORE_CREATE_PATH) + .reply(400) + .post(BEFORE_SIGN_IN_PATH) + .reply(400); + + const phoneNumber = TEST_PHONE_NUMBER; + const displayName = "Alice"; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("Authorization", "Bearer owner") + .send({ phoneNumber, displayName }) + .then((res) => { + expectStatusCode(200, res); + + // Shouldn't be set for authenticated requests: + expect(res.body).not.to.have.property("idToken"); + expect(res.body).not.to.have.property("refreshToken"); + + expect(res.body.displayName).to.equal(displayName); + expect(res.body.localId).to.be.a("string").and.not.empty; + }); + + // Shouldn't trigger nock calls + expect(nock.isDone()).to.be.false; + nock.cleanAll(); + }); + }); +}); diff --git a/src/emulator/auth/state.ts b/src/emulator/auth/state.ts index c1e01cb2cb5..5e74928515c 100644 --- a/src/emulator/auth/state.ts +++ b/src/emulator/auth/state.ts @@ -4,10 +4,11 @@ import { mirrorFieldTo, randomDigits, isValidPhoneNumber, + DeepPartial, } from "./utils"; import { MakeRequired } from "./utils"; import { AuthCloudFunction } from "./cloudFunctions"; -import { assert } from "./errors"; +import { assert, BadRequestError } from "./errors"; import { MfaEnrollments, Schemas } from "./types"; export const PROVIDER_PASSWORD = "password"; @@ -25,11 +26,10 @@ export abstract class ProjectState { private localIdForPhoneNumber: Map = new Map(); private localIdsForProviderEmail: Map> = new Map(); private userIdForProviderRawId: Map> = new Map(); - private refreshTokens: Map = new Map(); - private refreshTokensForLocalId: Map> = new Map(); private oobs: Map = new Map(); private verificationCodes: Map = new Map(); private temporaryProofs: Map = new Map(); + private pendingLocalIds: Set = new Set(); constructor(public readonly projectId: string) {} @@ -41,9 +41,9 @@ export abstract class ProjectState { abstract get oneAccountPerEmail(): boolean; - abstract get authCloudFunction(): AuthCloudFunction; + abstract get enableImprovedEmailPrivacy(): boolean; - abstract get usageMode(): UsageMode; + abstract get authCloudFunction(): AuthCloudFunction; abstract get allowPasswordSignup(): boolean; @@ -55,14 +55,22 @@ export abstract class ProjectState { abstract get enableEmailLinkSignin(): boolean; - createUser(props: Omit): UserInfo { + abstract shouldForwardCredentialToBlockingFunction( + type: "accessToken" | "idToken" | "refreshToken", + ): boolean; + + abstract getBlockingFunctionUri(event: BlockingFunctionEvents): string | undefined; + + generateLocalId(): string { for (let i = 0; i < 10; i++) { // Try this for 10 times to prevent ID collision (since our RNG is // Math.random() which isn't really that great). const localId = randomId(28); - const user = this.createUserWithLocalId(localId, props); - if (user) { - return user; + if (!this.users.has(localId) && !this.pendingLocalIds.has(localId)) { + // Create a pending localId until user is created. This creates a memory + // leak if a blocking functions throws and the localId is never used. + this.pendingLocalIds.add(localId); + return localId; } } // If we get 10 collisions in a row, there must be something very wrong. @@ -71,17 +79,15 @@ export abstract class ProjectState { createUserWithLocalId( localId: string, - props: Omit + props: Omit, ): UserInfo | undefined { if (this.users.has(localId)) { return undefined; } - const timestamp = new Date(); this.users.set(localId, { localId, - createdAt: props.createdAt || timestamp.getTime().toString(), - lastLoginAt: timestamp.getTime().toString(), }); + this.pendingLocalIds.delete(localId); const user = this.updateUserByLocalId(localId, props, { upsertProviders: props.providerUserInfo, @@ -98,7 +104,7 @@ export abstract class ProjectState { */ overwriteUserWithLocalId( localId: string, - props: Omit + props: Omit, ): UserInfo { const userInfoBefore = this.users.get(localId); if (userInfoBefore) { @@ -121,15 +127,6 @@ export abstract class ProjectState { deleteUser(user: UserInfo): void { this.users.delete(user.localId); this.removeUserFromIndex(user); - - const refreshTokens = this.refreshTokensForLocalId.get(user.localId); - if (refreshTokens) { - this.refreshTokensForLocalId.delete(user.localId); - for (const refreshToken of refreshTokens) { - this.refreshTokens.delete(refreshToken); - } - } - this.authCloudFunction.dispatch("delete", user); } @@ -139,7 +136,7 @@ export abstract class ProjectState { options: { upsertProviders?: ProviderUserInfo[]; deleteProviders?: string[]; - } = {} + } = {}, ): UserInfo { const upsertProviders = options.upsertProviders ?? []; const deleteProviders = options.deleteProviders ?? []; @@ -221,16 +218,16 @@ export abstract class ProjectState { for (const enrollment of enrollments) { assert( enrollment.phoneInfo && isValidPhoneNumber(enrollment.phoneInfo), - "INVALID_MFA_PHONE_NUMBER : Invalid format." + "INVALID_MFA_PHONE_NUMBER : Invalid format.", ); assert( enrollment.mfaEnrollmentId, - "INVALID_MFA_ENROLLMENT_ID : mfaEnrollmentId must be defined." + "INVALID_MFA_ENROLLMENT_ID : mfaEnrollmentId must be defined.", ); assert(!enrollmentIds.has(enrollment.mfaEnrollmentId), "DUPLICATE_MFA_ENROLLMENT_ID"); assert( !phoneNumbers.has(enrollment.phoneInfo), - "INTERNAL_ERROR : MFA Enrollment Phone Numbers must be unique." + "INTERNAL_ERROR : MFA Enrollment Phone Numbers must be unique.", ); phoneNumbers.add(enrollment.phoneInfo); enrollmentIds.add(enrollment.mfaEnrollmentId); @@ -241,7 +238,7 @@ export abstract class ProjectState { private updateUserProviderInfo( user: UserInfo, upsertProviders: ProviderUserInfo[], - deleteProviders: string[] + deleteProviders: string[], ): UserInfo { const oldProviderEmails = getProviderEmailsForUser(user); @@ -269,7 +266,7 @@ export abstract class ProjectState { users.set(upsert.rawId, user.localId); const index = user.providerUserInfo.findIndex( - (info) => info.providerId === upsert.providerId + (info) => info.providerId === upsert.providerId, ); if (index < 0) { user.providerUserInfo.push(upsert); @@ -373,7 +370,7 @@ export abstract class ProjectState { const info = user.providerUserInfo?.find((info) => info.providerId === provider); if (!info) { throw new Error( - `Internal assertion error: User ${localId} does not have providerInfo ${provider}.` + `Internal assertion error: User ${localId} does not have providerInfo ${provider}.`, ); } infos.push(info); @@ -394,42 +391,38 @@ export abstract class ProjectState { }: { extraClaims?: Record; secondFactor?: SecondFactorRecord; - } = {} + } = {}, ): string { const localId = userInfo.localId; - const refreshToken = randomBase64UrlStr(204); - this.refreshTokens.set(refreshToken, { + const refreshTokenRecord = { + _AuthEmulatorRefreshToken: "DO NOT MODIFY", localId, provider, extraClaims, + projectId: this.projectId, secondFactor, tenantId: userInfo.tenantId, - }); - let refreshTokens = this.refreshTokensForLocalId.get(localId); - if (!refreshTokens) { - refreshTokens = new Set(); - this.refreshTokensForLocalId.set(localId, refreshTokens); - } - refreshTokens.add(refreshToken); + }; + const refreshToken = encodeRefreshToken(refreshTokenRecord); return refreshToken; } - validateRefreshToken( - refreshToken: string - ): - | { - user: UserInfo; - provider: string; - extraClaims: Record; - secondFactor?: SecondFactorRecord; - } - | undefined { - const record = this.refreshTokens.get(refreshToken); - if (!record) { - return undefined; - } + validateRefreshToken(refreshToken: string): { + user: UserInfo; + provider: string; + extraClaims: Record; + secondFactor?: SecondFactorRecord; + } { + const record = decodeRefreshToken(refreshToken); + assert(record.projectId === this.projectId, "INVALID_REFRESH_TOKEN"); + if (this instanceof TenantProjectState) { + // Shouldn't ever reach this assertion, but adding for completeness + assert(record.tenantId === this.tenantId, "TENANT_ID_MISMATCH"); + } + const user = this.getUserByLocalId(record.localId); + assert(user, "INVALID_REFRESH_TOKEN"); return { - user: this.getUserByLocalIdAssertingExists(record.localId), + user, provider: record.provider, extraClaims: record.extraClaims, secondFactor: record.secondFactor, @@ -439,7 +432,7 @@ export abstract class ProjectState { createOob( email: string, requestType: OobRequestType, - generateLink: (oobCode: string) => string + generateLink: (oobCode: string) => string, ): OobRecord { const oobCode = randomBase64UrlStr(54); const oobLink = generateLink(oobCode); @@ -495,8 +488,6 @@ export abstract class ProjectState { this.localIdForPhoneNumber.clear(); this.localIdsForProviderEmail.clear(); this.userIdForProviderRawId.clear(); - this.refreshTokens.clear(); - this.refreshTokensForLocalId.clear(); // We do not clear OOBs / phone verification codes since some of those may // still be valid (e.g. email link / phone sign-in may still create a new @@ -516,7 +507,7 @@ export abstract class ProjectState { order: "ASC" | "DESC"; sortByField: "localId"; startToken?: string; - } + }, ): UserInfo[] { const users = []; for (const user of this.users.values()) { @@ -550,7 +541,7 @@ export abstract class ProjectState { validateTemporaryProof( temporaryProof: string, - phoneNumber: string + phoneNumber: string, ): TemporaryProofRecord | undefined { const record = this.temporaryProofs.get(temporaryProof); if (!record || record.phoneNumber !== phoneNumber) { @@ -584,10 +575,15 @@ export abstract class ProjectState { } export class AgentProjectState extends ProjectState { - private _oneAccountPerEmail = true; - private _usageMode = UsageMode.DEFAULT; private tenantProjectForTenantId: Map = new Map(); private readonly _authCloudFunction = new AuthCloudFunction(this.projectId); + private _config: Config = { + signIn: { allowDuplicateEmails: false }, + blockingFunctions: {}, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, + }; constructor(projectId: string) { super(projectId); @@ -598,19 +594,19 @@ export class AgentProjectState extends ProjectState { } get oneAccountPerEmail() { - return this._oneAccountPerEmail; + return !this._config.signIn.allowDuplicateEmails; } set oneAccountPerEmail(oneAccountPerEmail: boolean) { - this._oneAccountPerEmail = oneAccountPerEmail; + this._config.signIn.allowDuplicateEmails = !oneAccountPerEmail; } - get usageMode() { - return this._usageMode; + get enableImprovedEmailPrivacy() { + return !!this._config.emailPrivacyConfig.enableImprovedEmailPrivacy; } - set usageMode(usageMode: UsageMode) { - this._usageMode = usageMode; + set enableImprovedEmailPrivacy(improveEmailPrivacy: boolean) { + this._config.emailPrivacyConfig.enableImprovedEmailPrivacy = improveEmailPrivacy; } get allowPasswordSignup() { @@ -633,6 +629,56 @@ export class AgentProjectState extends ProjectState { return true; } + get config() { + return this._config; + } + + get blockingFunctionsConfig() { + return this._config.blockingFunctions; + } + + set blockingFunctionsConfig(blockingFunctions: BlockingFunctionsConfig) { + this._config.blockingFunctions = blockingFunctions; + } + + shouldForwardCredentialToBlockingFunction( + type: "accessToken" | "idToken" | "refreshToken", + ): boolean { + switch (type) { + case "accessToken": + return this._config.blockingFunctions.forwardInboundCredentials?.accessToken ?? false; + case "idToken": + return this._config.blockingFunctions.forwardInboundCredentials?.idToken ?? false; + case "refreshToken": + return this._config.blockingFunctions.forwardInboundCredentials?.refreshToken ?? false; + } + } + + getBlockingFunctionUri(event: BlockingFunctionEvents): string | undefined { + const triggers = this.blockingFunctionsConfig.triggers; + if (triggers) { + return Object.prototype.hasOwnProperty.call(triggers, event) + ? triggers![event].functionUri + : undefined; + } + return undefined; + } + + updateConfig( + update: Schemas["GoogleCloudIdentitytoolkitAdminV2Config"], + updateMask: string | undefined, + ): Config { + // Empty masks indicate a full update. + if (!updateMask) { + this.oneAccountPerEmail = !update.signIn?.allowDuplicateEmails ?? true; + this.blockingFunctionsConfig = update.blockingFunctions ?? {}; + this.enableImprovedEmailPrivacy = + update.emailPrivacyConfig?.enableImprovedEmailPrivacy ?? false; + return this.config; + } + return applyMask(updateMask, this.config, update); + } + getTenantProject(tenantId: string): TenantProjectState { if (!this.tenantProjectForTenantId.has(tenantId)) { // Implicitly creates tenant if it does not already exist and sets all @@ -693,7 +739,7 @@ export class AgentProjectState extends ProjectState { tenant.tenantId = tenantId; this.tenantProjectForTenantId.set( tenantId, - new TenantProjectState(this.projectId, tenantId, tenant, this) + new TenantProjectState(this.projectId, tenantId, tenant, this), ); return tenant; } @@ -708,7 +754,7 @@ export class TenantProjectState extends ProjectState { projectId: string, readonly tenantId: string, private _tenantConfig: Tenant, - private readonly parentProject: AgentProjectState + private readonly parentProject: AgentProjectState, ) { super(projectId); } @@ -717,12 +763,12 @@ export class TenantProjectState extends ProjectState { return this.parentProject.oneAccountPerEmail; } - get authCloudFunction() { - return this.parentProject.authCloudFunction; + get enableImprovedEmailPrivacy() { + return this.parentProject.enableImprovedEmailPrivacy; } - get usageMode() { - return this.parentProject.usageMode; + get authCloudFunction() { + return this.parentProject.authCloudFunction; } get tenantConfig() { @@ -749,13 +795,23 @@ export class TenantProjectState extends ProjectState { return this._tenantConfig.enableEmailLinkSignin; } + shouldForwardCredentialToBlockingFunction( + type: "accessToken" | "idToken" | "refreshToken", + ): boolean { + return this.parentProject.shouldForwardCredentialToBlockingFunction(type); + } + + getBlockingFunctionUri(event: BlockingFunctionEvents): string | undefined { + return this.parentProject.getBlockingFunctionUri(event); + } + delete(): void { this.parentProject.deleteTenant(this.tenantId); } updateTenant( update: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], - updateMask: string | undefined + updateMask: string | undefined, ): Tenant { // Empty masks indicate a full update if (!updateMask) { @@ -781,52 +837,7 @@ export class TenantProjectState extends ProjectState { return this.tenantConfig; } - const paths = updateMask.split(","); - for (const path of paths) { - const fields = path.split("."); - // Using `any` here to recurse over Tenant config objects - let updateField: any = update; - let existingField: any = this._tenantConfig; - let field; - for (let i = 0; i < fields.length - 1; i++) { - field = fields[i]; - - // Doesn't exist on update - if (updateField[field] == null) { - console.warn(`Unable to find field '${field}' in update '${updateField}`); - break; - } - - // Field on existing is an array or is a primitive (i.e. cannot index - // any further) - if ( - Array.isArray(updateField[field]) || - Object(updateField[field]) !== updateField[field] - ) { - console.warn(`Field '${field}' is singular and cannot have sub-fields`); - break; - } - - // Non-standard behavior, this creates new fields regardless of if the - // final field is set. Typical behavior would not modify the config - // payload if the final field is not successfully set. - if (!existingField[field]) { - existingField[field] = {}; - } - - updateField = updateField[field]; - existingField = existingField[field]; - } - // Reassign final field if possible - field = fields[fields.length - 1]; - if (updateField[field] == null) { - console.warn(`Unable to find field '${field}' in update '${JSON.stringify(updateField)}`); - continue; - } - existingField[field] = updateField[field]; - } - - return this.tenantConfig; + return applyMask(updateMask, this.tenantConfig, update); } } @@ -853,10 +864,32 @@ export type Tenant = Omit< "testPhoneNumbers" | "mfaConfig" > & { tenantId: string; mfaConfig: MfaConfig }; -interface RefreshTokenRecord { +export type SignInConfig = MakeRequired< + Schemas["GoogleCloudIdentitytoolkitAdminV2SignInConfig"], + "allowDuplicateEmails" +>; + +export type BlockingFunctionsConfig = + Schemas["GoogleCloudIdentitytoolkitAdminV2BlockingFunctionsConfig"]; + +export type EmailPrivacyConfig = Schemas["GoogleCloudIdentitytoolkitAdminV2EmailPrivacyConfig"]; + +// Serves as a substitute for Schemas["GoogleCloudIdentitytoolkitAdminV2Config"], +// i.e. the configuration object for top-level AgentProjectStates. Emulator +// fixes certain configurations for ease of use / testing, so as non-standard +// behavior, Config only stores the configurable fields. +export type Config = { + signIn: SignInConfig; + blockingFunctions: BlockingFunctionsConfig; + emailPrivacyConfig: EmailPrivacyConfig; +}; + +export interface RefreshTokenRecord { + _AuthEmulatorRefreshToken: string; localId: string; provider: string; extraClaims: Record; + projectId: string; secondFactor?: SecondFactorRecord; tenantId?: string; } @@ -883,6 +916,11 @@ export interface PhoneVerificationRecord { sessionInfo: string; } +export enum BlockingFunctionEvents { + BEFORE_CREATE = "beforeCreate", + BEFORE_SIGN_IN = "beforeSignIn", +} + interface TemporaryProofRecord { phoneNumber: string; temporaryProof: string; @@ -891,6 +929,22 @@ interface TemporaryProofRecord { // a bit easier. Therefore, there's no need to record createdAt timestamps. } +export function encodeRefreshToken(refreshTokenRecord: RefreshTokenRecord): string { + return Buffer.from(JSON.stringify(refreshTokenRecord), "utf8").toString("base64"); +} + +export function decodeRefreshToken(refreshTokenString: string): RefreshTokenRecord { + let refreshTokenRecord: RefreshTokenRecord; + try { + const json = Buffer.from(refreshTokenString, "base64").toString("utf8"); + refreshTokenRecord = JSON.parse(json) as RefreshTokenRecord; + } catch { + throw new BadRequestError("INVALID_REFRESH_TOKEN"); + } + assert(refreshTokenRecord._AuthEmulatorRefreshToken, "INVALID_REFRESH_TOKEN"); + return refreshTokenRecord; +} + function getProviderEmailsForUser(user: UserInfo): Set { const emails = new Set(); user.providerUserInfo?.forEach(({ email }) => { @@ -901,7 +955,57 @@ function getProviderEmailsForUser(user: UserInfo): Set { return emails; } -export enum UsageMode { - DEFAULT = "DEFAULT", - PASSTHROUGH = "PASSTHROUGH", +/** + * Updates fields based on specified update mask. Note that this is a no-op if + * the update mask is empty. + * + * @param updateMask a comma separated list of fully qualified names of fields + * @param dest the destination to apply updates to + * @param update the updates to apply + * @returns the updated destination object + */ +function applyMask(updateMask: string, dest: T, update: DeepPartial): T { + const paths = updateMask.split(","); + for (const path of paths) { + const fields = path.split("."); + // Using `any` here to recurse over destination objects + let updateField: any = update; + let existingField: any = dest; + let field; + for (let i = 0; i < fields.length - 1; i++) { + field = fields[i]; + + // Doesn't exist on update + if (updateField[field] == null) { + console.warn(`Unable to find field '${field}' in update '${updateField}`); + break; + } + + // Field on existing is an array or is a primitive (i.e. cannot index + // any further) + if (Array.isArray(updateField[field]) || Object(updateField[field]) !== updateField[field]) { + console.warn(`Field '${field}' is singular and cannot have sub-fields`); + break; + } + + // Non-standard behavior, this creates new fields regardless of if the + // final field is set. Typical behavior would not modify the config + // payload if the final field is not successfully set. + if (!existingField[field]) { + existingField[field] = {}; + } + + updateField = updateField[field]; + existingField = existingField[field]; + } + // Reassign final field if possible + field = fields[fields.length - 1]; + if (updateField[field] == null) { + console.warn(`Unable to find field '${field}' in update '${JSON.stringify(updateField)}`); + continue; + } + existingField[field] = updateField[field]; + } + + return dest; } diff --git a/src/test/emulators/auth/tenant.spec.ts b/src/emulator/auth/tenant.spec.ts similarity index 97% rename from src/test/emulators/auth/tenant.spec.ts rename to src/emulator/auth/tenant.spec.ts index 31bab04f525..8fc5697ff16 100644 --- a/src/test/emulators/auth/tenant.spec.ts +++ b/src/emulator/auth/tenant.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; -import { Tenant } from "../../../emulator/auth/state"; -import { expectStatusCode, registerTenant } from "./helpers"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; +import { Tenant } from "./state"; +import { expectStatusCode, registerTenant } from "./testing/helpers"; +import { describeAuthEmulator } from "./testing/setup"; describeAuthEmulator("tenant management", ({ authApi }) => { describe("createTenant", () => { @@ -125,7 +125,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { await authApi() .delete( - `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, ) .set("Authorization", "Bearer owner") .then((res) => { @@ -139,7 +139,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { await authApi() .delete( - `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, ) .set("Authorization", "Bearer owner") // Sets content-type and sends "{}" in request payload. This is very @@ -242,7 +242,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { await authApi() .patch( - `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, ) .set("Authorization", "Bearer owner") .query({ updateMask }) @@ -278,7 +278,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { await authApi() .patch( - `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, ) .set("Authorization", "Bearer owner") .query({ updateMask }) @@ -301,7 +301,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { await authApi() .patch( - `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, ) .set("Authorization", "Bearer owner") .query({ updateMask }) @@ -324,7 +324,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { await authApi() .patch( - `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, ) .set("Authorization", "Bearer owner") .send({ @@ -358,7 +358,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { await authApi() .patch( - `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, ) .set("Authorization", "Bearer owner") .send({}) diff --git a/src/test/emulators/auth/helpers.ts b/src/emulator/auth/testing/helpers.ts similarity index 87% rename from src/test/emulators/auth/helpers.ts rename to src/emulator/auth/testing/helpers.ts index 1651a5c893d..4f9d1809b5a 100644 --- a/src/test/emulators/auth/helpers.ts +++ b/src/emulator/auth/testing/helpers.ts @@ -2,10 +2,10 @@ import { STATUS_CODES } from "http"; import { inspect } from "util"; import * as supertest from "supertest"; import { expect, AssertionError } from "chai"; -import { IdpJwtPayload } from "../../../emulator/auth/operations"; -import { OobRecord, PhoneVerificationRecord, Tenant, UserInfo } from "../../../emulator/auth/state"; +import { IdpJwtPayload } from "../operations"; +import { OobRecord, PhoneVerificationRecord, Tenant, UserInfo } from "../state"; import { TestAgent, PROJECT_ID } from "./setup"; -import { MfaEnrollments, Schemas } from "../../../emulator/auth/types"; +import { MfaEnrollment, MfaEnrollments, Schemas } from "../types"; export { PROJECT_ID }; export const TEST_PHONE_NUMBER = "+15555550100"; @@ -17,6 +17,8 @@ export const TEST_MFA_INFO = { phoneInfo: TEST_PHONE_NUMBER, }; export const TEST_INVALID_PHONE_NUMBER = "5555550100"; /* no country code */ +export const DISPLAY_NAME = "Example User"; +export const PHOTO_URL = "http://fakephotourl.test"; export const FAKE_GOOGLE_ACCOUNT = { displayName: "Example User", email: "example@gmail.com", @@ -50,6 +52,13 @@ export const REAL_GOOGLE_ACCOUNT = { "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZiYzYzZTlmMThkNTYxYjM0ZjU2NjhmODhhZTI3ZDQ4ODc2ZDgwNzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNTExMzIzNjU2NjY4MzM5ODMwMSIsImF0X2hhc2giOiJJRHA0UFFldFItLUFyaWhXX2NYMmd3IiwiaWF0IjoxNTk3ODgyNDQyLCJleHAiOjE1OTc4ODYwNDJ9.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }; +// Used for testing blocking functions +export const BLOCKING_FUNCTION_HOST = "http://my-blocking-function.test"; +export const BEFORE_CREATE_PATH = "/beforeCreate"; +export const BEFORE_SIGN_IN_PATH = "/beforeSignIn"; +export const BEFORE_CREATE_URL = BLOCKING_FUNCTION_HOST + BEFORE_CREATE_PATH; +export const BEFORE_SIGN_IN_URL = BLOCKING_FUNCTION_HOST + BEFORE_SIGN_IN_PATH; + /** * Asserts that the response has the expected status code. * @param expected the expected status code @@ -61,7 +70,7 @@ export function expectStatusCode(expected: number, res: supertest.Response): voi throw new AssertionError( `expected ${expected} "${STATUS_CODES[expected]}", got ${res.status} "${ STATUS_CODES[res.status] - }", with response body:\n${body}` + }", with response body:\n${body}`, ); } } @@ -79,7 +88,7 @@ export function fakeClaims(input: Partial & { sub: string }): Idp exp: 1597974008, iat: 1597970408, }, - input + input, ); } @@ -94,7 +103,7 @@ export function registerUser( displayName?: string; mfaInfo?: MfaEnrollments; tenantId?: string; - } + }, ): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { return testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") @@ -112,7 +121,7 @@ export function registerUser( } export function registerAnonUser( - testAgent: TestAgent + testAgent: TestAgent, ): Promise<{ idToken: string; localId: string; refreshToken: string }> { return testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") @@ -131,7 +140,7 @@ export function registerAnonUser( export async function signInWithEmailLink( testAgent: TestAgent, email: string, - idTokenToLink?: string + idTokenToLink?: string, ): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { const { oobCode } = await createEmailSignInOob(testAgent, email); @@ -152,8 +161,28 @@ export async function signInWithEmailLink( export function signInWithPassword( testAgent: TestAgent, email: string, - password: string -): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { + password: string, + extractMfaPending: boolean = false, +): Promise<{ + idToken?: string; + localId?: string; + refreshToken?: string; + email?: string; + mfaPendingCredential?: string; + mfaEnrollmentId?: string; +}> { + if (extractMfaPending) { + return testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .send({ email, password }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(200, res); + const mfaPendingCredential = res.body.mfaPendingCredential as string; + const mfaInfo = res.body.mfaInfo as MfaEnrollment[]; + return { mfaPendingCredential, mfaEnrollmentId: mfaInfo[0].mfaEnrollmentId }; + }); + } return testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") .send({ email, password }) @@ -171,7 +200,7 @@ export function signInWithPassword( export async function signInWithPhoneNumber( testAgent: TestAgent, - phoneNumber: string + phoneNumber: string, ): Promise<{ idToken: string; localId: string; refreshToken: string }> { const sessionInfo = await testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") @@ -202,7 +231,7 @@ export function signInWithFakeClaims( testAgent: TestAgent, providerId: string, claims: Partial & { sub: string }, - tenantId?: string + tenantId?: string, ): Promise<{ idToken: string; localId: string; refreshToken: string; email?: string }> { const fakeIdToken = JSON.stringify(fakeClaims(claims)); return testAgent @@ -210,7 +239,7 @@ export function signInWithFakeClaims( .query({ key: "fake-api-key" }) .send({ postBody: `providerId=${encodeURIComponent(providerId)}&id_token=${encodeURIComponent( - fakeIdToken + fakeIdToken, )}`, requestUri: "http://localhost", returnIdpCredential: true, @@ -231,7 +260,7 @@ export function signInWithFakeClaims( export async function expectUserNotExistsForIdToken( testAgent: TestAgent, idToken: string, - tenantId?: string + tenantId?: string, ): Promise { await testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") @@ -257,7 +286,7 @@ export async function expectIdTokenExpired(testAgent: TestAgent, idToken: string export function getAccountInfoByIdToken( testAgent: TestAgent, idToken: string, - tenantId?: string + tenantId?: string, ): Promise { return testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") @@ -273,7 +302,7 @@ export function getAccountInfoByIdToken( export function getAccountInfoByLocalId( testAgent: TestAgent, localId: string, - tenantId?: string + tenantId?: string, ): Promise { return testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") @@ -298,7 +327,7 @@ export function inspectOobs(testAgent: TestAgent, tenantId?: string): Promise { const path = tenantId ? `/emulator/v1/projects/${PROJECT_ID}/tenants/${tenantId}/verificationCodes` @@ -312,7 +341,7 @@ export function inspectVerificationCodes( export function createEmailSignInOob( testAgent: TestAgent, email: string, - tenantId?: string + tenantId?: string, ): Promise<{ oobCode: string; oobLink: string }> { return testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") @@ -351,7 +380,7 @@ export function updateProjectConfig(testAgent: TestAgent, config: {}): Promise { return testAgent .post("/identitytoolkit.googleapis.com/v1/accounts:update") @@ -366,7 +395,7 @@ export async function enrollPhoneMfa( testAgent: TestAgent, idToken: string, phoneNumber: string, - tenantId?: string + tenantId?: string, ): Promise<{ idToken: string; refreshToken: string }> { const sessionInfo = await testAgent .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") @@ -407,7 +436,7 @@ export function deleteAccount(testAgent: TestAgent, reqBody: {}): Promise { return testAgent .post(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) @@ -419,3 +448,19 @@ export function registerTenant( return res.body; }); } + +export async function updateConfig( + testAgent: TestAgent, + projectId: string, + config: Schemas["GoogleCloudIdentitytoolkitAdminV2Config"], + updateMask?: string, +): Promise { + await testAgent + .patch(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/config`) + .set("Authorization", "Bearer owner") + .query({ updateMask }) + .send(config) + .then((res) => { + expectStatusCode(200, res); + }); +} diff --git a/src/emulator/auth/testing/setup.ts b/src/emulator/auth/testing/setup.ts new file mode 100644 index 00000000000..1fab8a4117e --- /dev/null +++ b/src/emulator/auth/testing/setup.ts @@ -0,0 +1,60 @@ +import { Suite } from "mocha"; +import { useFakeTimers } from "sinon"; +import * as supertest from "supertest"; +import { createApp } from "../server"; +import { AgentProjectState } from "../state"; +import { SingleProjectMode } from ".."; + +export const PROJECT_ID = "example"; + +/** + * Describe a test suite about the Auth Emulator, with server setup properly. + * @param title the title of the test suite + * @param fn the callback where the suite is defined + * @return the mocha test suite + */ +export function describeAuthEmulator( + title: string, + fn: (this: Suite, utils: AuthTestUtils) => void, + singleProjectMode = SingleProjectMode.NO_WARNING, +): Suite { + return describe(`Auth Emulator: ${title}`, function (this) { + let authApp: Express.Application; + beforeEach("setup or reuse auth server", async function (this) { + this.timeout(20000); + authApp = await createOrReuseApp(singleProjectMode); + }); + + let clock: sinon.SinonFakeTimers; + beforeEach(() => { + clock = useFakeTimers(); + }); + afterEach(() => clock.restore()); + return fn.call(this, { authApi: () => supertest(authApp), getClock: () => clock }); + }); +} + +export type TestAgent = supertest.SuperTest; + +export type AuthTestUtils = { + authApi: () => TestAgent; + getClock: () => sinon.SinonFakeTimers; +}; + +// Keep a global auth server since start-up takes too long: +const cachedAuthAppMap = new Map(); +const projectStateForId = new Map(); + +async function createOrReuseApp( + singleProjectMode: SingleProjectMode, +): Promise { + let cachedAuthApp: Express.Application | undefined = cachedAuthAppMap.get(singleProjectMode); + if (cachedAuthApp === undefined) { + cachedAuthApp = await createApp(PROJECT_ID, singleProjectMode, projectStateForId); + cachedAuthAppMap.set(singleProjectMode, cachedAuthApp); + } + // Clear the state every time to make it work like brand new. + // NOTE: This probably won't work with parallel mode if we ever enable it. + projectStateForId.clear(); + return cachedAuthApp; +} diff --git a/src/emulator/auth/utils.ts b/src/emulator/auth/utils.ts index b86925f20ff..ae535d23349 100644 --- a/src/emulator/auth/utils.ts +++ b/src/emulator/auth/utils.ts @@ -9,6 +9,13 @@ import { EmulatorLogger } from "../emulatorLogger"; */ export type MakeRequired = T & Required>; +/** + * Utility type to make all fields recursively optional. + */ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + /** * Checks if email looks like a valid email address. * @@ -157,54 +164,24 @@ export function logError(err: Error): void { } EmulatorLogger.forEmulator(Emulators.AUTH).log( "WARN", - err.stack || err.message || err.constructor.name + err.stack || err.message || err.constructor.name, ); } /** * Return a URL object with Auth Emulator protocol, host, and port populated. - * @param req a express request to emulator server; used to infer host - * @return the construted URL object + * + * Compared to EmulatorRegistry.url, this functions prefers the configured host + * and port, which are likely more useful when the link is opened on the same + * device running the emulator (assuming developers click on the link printed on + * terminal or Emulator UI). */ export function authEmulatorUrl(req: express.Request): URL { - // WHATWG URL API has no way to create from parts, so let's use a minimal - // working URL as a starting point. (Let's avoid legacy Node.js `url.format`). - const url = new URL("http://localhost/"); - - // Prefer configured host and port since the link will be most likely opened - // on the same device running the emulator (assuming developers click on the - // link printed on terminal or Emulator UI). - // TODO(yuchenshi): Extract these logic into common emulator utils. - const info = EmulatorRegistry.getInfo(Emulators.AUTH); - if (info) { - // If listening to all IPv4/6 addresses, use loopback addresses instead. - // All-zero addresses are invalid and not tolerated by some browsers / platforms. - // See: https://github.com/firebase/firebase-tools-ui/issues/286 - if (info.host === "0.0.0.0") { - url.hostname = "127.0.0.1"; - } else if (info.host === "::") { - url.hostname = "[::1]"; - } else if (info.host.includes(":")) { - url.hostname = `[${info.host}]`; // IPv6 addresses need to be quoted using brackets. - } else { - url.hostname = info.host; - } - url.port = info.port.toString(); + if (EmulatorRegistry.isRunning(Emulators.AUTH)) { + return EmulatorRegistry.url(Emulators.AUTH); } else { - // Or we can try the Host request header, since it contains hostname + port - // already and has been proven working (since we've got the client request). - const host = req.headers.host; - url.protocol = req.protocol; - - if (host) { - url.host = host; - } else { - // This can probably only happen during testing, but let's warn anyway. - console.warn("Cannot determine host and port of auth emulator server."); - } + return EmulatorRegistry.url(Emulators.AUTH, req); } - - return url; } /** @@ -216,7 +193,7 @@ export function authEmulatorUrl(req: express.Request): URL { export function mirrorFieldTo( dest: D, field: K, - source: { [KK in K]?: D[KK] } + source: { [KK in K]?: D[KK] }, ): void { const value = source[field] as D[K] | undefined; if (value === undefined) { diff --git a/src/emulator/commandUtils.spec.ts b/src/emulator/commandUtils.spec.ts new file mode 100644 index 00000000000..0fb8da2f04b --- /dev/null +++ b/src/emulator/commandUtils.spec.ts @@ -0,0 +1,123 @@ +import * as commandUtils from "./commandUtils"; +import { expect } from "chai"; +import { FirebaseError } from "../error"; +import { EXPORT_ON_EXIT_USAGE_ERROR, EXPORT_ON_EXIT_CWD_DANGER } from "./commandUtils"; +import * as path from "path"; +import * as sinon from "sinon"; + +describe("commandUtils", () => { + const testSetExportOnExitOptions = (options: any): any => { + commandUtils.setExportOnExitOptions(options); + return options; + }; + + describe("Mocked path resolve", () => { + const mockCWD = "/a/resolved/path/example"; + const mockDestinationDir = "/path/example"; + + let pathStub: sinon.SinonStub; + beforeEach(() => { + pathStub = sinon.stub(path, "resolve").callsFake((path) => { + return path === "." ? mockCWD : mockDestinationDir; + }); + }); + + afterEach(() => { + pathStub.restore(); + }); + + it("Should not block if destination contains a match to the CWD", () => { + const directoryToAllow = mockDestinationDir; + expect(testSetExportOnExitOptions({ exportOnExit: directoryToAllow }).exportOnExit).to.equal( + directoryToAllow, + ); + }); + }); + + /** + * Currently, setting the --export-on-exit as the current CWD can inflict on + * full directory deletion + */ + const directoriesThatShouldFail = [ + ".", // The current dir + "./", // The current dir with / + path.resolve("."), // An absolute path + path.resolve(".."), // A folder that directs to the CWD + path.resolve("../.."), // Another folder that directs to the CWD + ]; + + directoriesThatShouldFail.forEach((dir) => { + it(`Should disallow the user to set the current folder (ex: ${dir}) as --export-on-exit option`, () => { + expect(() => testSetExportOnExitOptions({ exportOnExit: dir })).to.throw( + EXPORT_ON_EXIT_CWD_DANGER, + ); + const cwdSubDir = path.join(dir, "some-dir"); + expect(testSetExportOnExitOptions({ exportOnExit: cwdSubDir }).exportOnExit).to.equal( + cwdSubDir, + ); + }); + }); + + it("Should disallow the user to set the current folder via the --import flag", () => { + expect(() => testSetExportOnExitOptions({ import: ".", exportOnExit: true })).to.throw( + EXPORT_ON_EXIT_CWD_DANGER, + ); + const cwdSubDir = path.join(".", "some-dir"); + expect( + testSetExportOnExitOptions({ import: cwdSubDir, exportOnExit: true }).exportOnExit, + ).to.equal(cwdSubDir); + }); + + it("should validate --export-on-exit options", () => { + expect(testSetExportOnExitOptions({ import: "./data" }).exportOnExit).to.be.undefined; + expect( + testSetExportOnExitOptions({ import: "./data", exportOnExit: "./data" }).exportOnExit, + ).to.eql("./data"); + expect( + testSetExportOnExitOptions({ import: "./data", exportOnExit: "./dataExport" }).exportOnExit, + ).to.eql("./dataExport"); + expect( + testSetExportOnExitOptions({ import: "./data", exportOnExit: true }).exportOnExit, + ).to.eql("./data"); + expect(() => testSetExportOnExitOptions({ exportOnExit: true })).to.throw( + FirebaseError, + EXPORT_ON_EXIT_USAGE_ERROR, + ); + expect(() => testSetExportOnExitOptions({ import: "", exportOnExit: true })).to.throw( + FirebaseError, + EXPORT_ON_EXIT_USAGE_ERROR, + ); + expect(() => testSetExportOnExitOptions({ import: "", exportOnExit: "" })).to.throw( + FirebaseError, + EXPORT_ON_EXIT_USAGE_ERROR, + ); + }); + it("should delete the --import option when the dir does not exist together with --export-on-exit", () => { + expect( + testSetExportOnExitOptions({ + import: "./dataDirThatDoesNotExist", + exportOnExit: "./dataDirThatDoesNotExist", + }).import, + ).to.be.undefined; + const options = testSetExportOnExitOptions({ + import: "./dataDirThatDoesNotExist", + exportOnExit: true, + }); + expect(options.import).to.be.undefined; + expect(options.exportOnExit).to.eql("./dataDirThatDoesNotExist"); + }); + it("should not touch the --import option when the dir does not exist but --export-on-exit is not set", () => { + expect( + testSetExportOnExitOptions({ + import: "./dataDirThatDoesNotExist", + }).import, + ).to.eql("./dataDirThatDoesNotExist"); + }); + it("should keep other unrelated options when using setExportOnExitOptions", () => { + expect( + testSetExportOnExitOptions({ + someUnrelatedOption: "isHere", + }).someUnrelatedOption, + ).to.eql("isHere"); + }); +}); diff --git a/src/emulator/commandUtils.ts b/src/emulator/commandUtils.ts index df9b9a5d1f5..6722fe3222a 100644 --- a/src/emulator/commandUtils.ts +++ b/src/emulator/commandUtils.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as childProcess from "child_process"; import * as controller from "../emulator/controller"; @@ -8,18 +8,18 @@ import { logger } from "../logger"; import * as path from "path"; import { Constants } from "./constants"; import { requireAuth } from "../requireAuth"; -import requireConfig = require("../requireConfig"); +import { requireConfig } from "../requireConfig"; import { Emulators, ALL_SERVICE_EMULATORS } from "./types"; import { FirebaseError } from "../error"; import { EmulatorRegistry } from "./registry"; -import { FirestoreEmulator } from "./firestoreEmulator"; import { getProjectId } from "../projectUtils"; import { promptOnce } from "../prompt"; -import { onExit } from "./controller"; import * as fsutils from "../fsutils"; import Signals = NodeJS.Signals; import SignalsListener = NodeJS.SignalsListener; -import Table = require("cli-table"); +const Table = require("cli-table"); +import { emulatorSession } from "../track"; +import { setEnvVarsForEmulators } from "./env"; export const FLAG_ONLY = "--only "; export const DESC_ONLY = @@ -45,6 +45,12 @@ export const EXPORT_ON_EXIT_USAGE_ERROR = `"${FLAG_EXPORT_ON_EXIT_NAME}" must be used with "${FLAG_IMPORT}"` + ` or provide a dir directly to "${FLAG_EXPORT_ON_EXIT}"`; +export const EXPORT_ON_EXIT_CWD_DANGER = `"${FLAG_EXPORT_ON_EXIT_NAME}" must not point to the current directory or parents. Please choose a new/dedicated directory for exports.`; + +export const FLAG_VERBOSITY_NAME = "--log-verbosity"; +export const FLAG_VERBOSITY = `${FLAG_VERBOSITY_NAME} `; +export const DESC_VERBOSITY = "One of: DEBUG, INFO, QUIET, SILENT. "; // TODO complete the rest + export const FLAG_UI = "--ui"; export const DESC_UI = "run the Emulator UI"; @@ -57,14 +63,26 @@ export const FLAG_TEST_PARAMS = "--test-params "; export const DESC_TEST_PARAMS = "A .env file containing test param values for your emulated extension."; -const DEFAULT_CONFIG = new Config( - { database: {}, firestore: {}, functions: {}, hosting: {}, emulators: { auth: {}, pubsub: {} } }, - {} +export const DEFAULT_CONFIG = new Config( + { + eventarc: {}, + database: {}, + firestore: {}, + functions: {}, + hosting: {}, + emulators: { auth: {}, pubsub: {} }, + }, + {}, ); +/** + * Utility to be put in the "before" handler for a RTDB or Firestore command + * that supports the emulator. Prints a warning when environment variables + * specify an emulator address. + */ export function printNoticeIfEmulated( options: any, - emulator: Emulators.DATABASE | Emulators.FIRESTORE + emulator: Emulators.DATABASE | Emulators.FIRESTORE, ): void { if (emulator !== Emulators.DATABASE && emulator !== Emulators.FIRESTORE) { return; @@ -79,15 +97,20 @@ export function printNoticeIfEmulated( if (envVal) { utils.logBullet( `You have set ${clc.bold( - `${envKey}=${envVal}` - )}, this command will execute against the ${emuName} running at that address.` + `${envKey}=${envVal}`, + )}, this command will execute against the ${emuName} running at that address.`, ); } } +/** + * Utility to be put in the "before" handler for a RTDB or Firestore command + * that always talks to production. This warns customers if they've specified + * an emulator port that the command actually talks to production. + */ export function warnEmulatorNotSupported( options: any, - emulator: Emulators.DATABASE | Emulators.FIRESTORE + emulator: Emulators.DATABASE | Emulators.FIRESTORE, ): void | Promise { if (emulator !== Emulators.DATABASE && emulator !== Emulators.FIRESTORE) { return; @@ -103,8 +126,8 @@ export function warnEmulatorNotSupported( if (envVal) { utils.logWarning( `You have set ${clc.bold( - `${envKey}=${envVal}` - )}, however this command does not support running against the ${emuName} so this action will affect production.` + `${envKey}=${envVal}`, + )}, however this command does not support running against the ${emuName} so this action will affect production.`, ); const opts = { @@ -114,7 +137,7 @@ export function warnEmulatorNotSupported( type: "confirm", default: false, message: "Do you want to continue?", - }).then((confirm: boolean) => { + }).then(() => { if (!opts.confirm) { return utils.reject("Command aborted.", { exit: 1 }); } @@ -122,6 +145,10 @@ export function warnEmulatorNotSupported( } } +/** + * Utility method to be inserted in the "before" function for a command that + * uses the emulator suite. + */ export async function beforeEmulatorCommand(options: any): Promise { const optionsWithDefaultConfig = { ...options, @@ -139,13 +166,13 @@ export async function beforeEmulatorCommand(options: any): Promise { try { await requireAuth(options); - } catch (e) { + } catch (e: any) { logger.debug(e); utils.logLabeledWarning( "emulators", `You are not currently authenticated so some features may not work correctly. Please run ${clc.bold( - "firebase login" - )} to authenticate the CLI.` + "firebase login", + )} to authenticate the CLI.`, ); } @@ -157,22 +184,33 @@ export async function beforeEmulatorCommand(options: any): Promise { } } -export function parseInspectionPort(options: any): number { - let port = options.inspectFunctions; - if (port === true) { - port = "9229"; +/** + * Returns a literal port number if specified or true | false if enabled. + * A true value will later be turned into a dynamic port. + */ +export function parseInspectionPort(options: any): number | boolean { + const port = options.inspectFunctions; + if (typeof port === "undefined") { + return false; + } else if (typeof port === "boolean") { + return port; } const parsed = Number(port); if (isNaN(parsed) || parsed < 1024 || parsed > 65535) { throw new FirebaseError( - `"${port}" is not a valid port for debugging, please pass an integer between 1024 and 65535.` + `"${port}" is not a valid port for debugging, please pass an integer between 1024 and 65535 or true for a dynamic port.`, ); } return parsed; } +export interface ExportOnExitOptions { + exportOnExit?: boolean | string; + import?: string; +} + /** * Sets the correct export options based on --import and --export-on-exit. Mutates the options object. * Also validates if we have a correct setting we need to export the data on exit. @@ -181,7 +219,7 @@ export function parseInspectionPort(options: any): number { * export data the first time they start developing on a clean project. * @param options */ -export function setExportOnExitOptions(options: any) { +export function setExportOnExitOptions(options: ExportOnExitOptions): void { if (options.exportOnExit || typeof options.exportOnExit === "string") { // note that options.exportOnExit may be a bool when used as a flag without a [dir] argument: // --import ./data --export-on-exit @@ -203,15 +241,19 @@ export function setExportOnExitOptions(options: any) { // firebase emulators:start --debug --import '' --export-on-exit '' throw new FirebaseError(EXPORT_ON_EXIT_USAGE_ERROR); } + + if (path.resolve(".").startsWith(path.resolve(options.exportOnExit))) { + throw new FirebaseError(EXPORT_ON_EXIT_CWD_DANGER); + } } return; } function processKillSignal( signal: Signals, - res: (value?: unknown) => void, + res: (value?: void) => void, rej: (value?: unknown) => void, - options: any + options: any, ): SignalsListener { let lastSignal = new Date().getTime(); let signalCount = 0; @@ -235,19 +277,19 @@ function processKillSignal( if (signalCount === 1) { utils.logLabeledBullet( "emulators", - `Received ${signalDisplay} for the first time. Starting a clean shutdown.` + `Received ${signalDisplay} for the first time. Starting a clean shutdown.`, ); utils.logLabeledBullet( "emulators", - `Please wait for a clean shutdown or send the ${signalDisplay} signal again to stop right now.` + `Please wait for a clean shutdown or send the ${signalDisplay} signal again to stop right now.`, ); // in case of a double 'Ctrl-C' we do not want to cleanly exit with onExit/cleanShutdown - await onExit(options); + await controller.onExit(options); await controller.cleanShutdown(); } else { logger.debug(`Skipping clean onExit() and cleanShutdown()`); const runningEmulatorsInfosWithPid = EmulatorRegistry.listRunningWithInfo().filter((i) => - Boolean(i.pid) + Boolean(i.pid), ); utils.logLabeledWarning( @@ -256,7 +298,7 @@ function processKillSignal( runningEmulatorsInfosWithPid.length } subprocess${ runningEmulatorsInfosWithPid.length > 1 ? "es" : "" - } to finish. These processes ${clc.bold("may")} still be running on your machine: ` + } to finish. These processes ${clc.bold("may")} still be running on your machine: `, ); const pids: number[] = []; @@ -272,7 +314,7 @@ function processKillSignal( pids.push(emulatorInfo.pid as number); emulatorsTable.push([ Constants.description(emulatorInfo.name), - EmulatorRegistry.getInfoHostString(emulatorInfo), + getListenOverview(emulatorInfo.name) ?? "unknown", emulatorInfo.pid, ]); } @@ -284,75 +326,53 @@ function processKillSignal( } } res(); - } catch (e) { + } catch (e: any) { logger.debug(e); rej(); } }; } +/** + * Returns a promise that resolves when killing signals are received and processed. + * + * Fulfilled or rejected depending on the processing result (e.g. exporting). + * @return a promise that is pending until signals received and processed + */ export function shutdownWhenKilled(options: any): Promise { - return new Promise((res, rej) => { + return new Promise((res, rej) => { ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"].forEach((signal: string) => { process.on(signal as Signals, processKillSignal(signal as Signals, res, rej, options)); }); - }) - .then(() => { - process.exit(0); - }) - .catch((e) => { - logger.debug(e); - utils.logLabeledWarning( - "emulators", - "emulators failed to shut down cleanly, see firebase-debug.log for details." - ); - process.exit(1); - }); + }).catch((e) => { + logger.debug(e); + utils.logLabeledWarning( + "emulators", + "emulators failed to shut down cleanly, see firebase-debug.log for details.", + ); + throw e; + }); } async function runScript(script: string, extraEnv: Record): Promise { utils.logBullet(`Running script: ${clc.bold(script)}`); const env: NodeJS.ProcessEnv = { ...process.env, ...extraEnv }; - - const databaseInstance = EmulatorRegistry.get(Emulators.DATABASE); - if (databaseInstance) { - const info = databaseInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = address; - } - - const firestoreInstance = EmulatorRegistry.get(Emulators.FIRESTORE); - if (firestoreInstance) { - const info = firestoreInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - - env[Constants.FIRESTORE_EMULATOR_HOST] = address; - env[FirestoreEmulator.FIRESTORE_EMULATOR_ENV_ALT] = address; - } - - const storageInstance = EmulatorRegistry.get(Emulators.STORAGE); - if (storageInstance) { - const info = storageInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - - env[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = address; - env[Constants.CLOUD_STORAGE_EMULATOR_HOST] = `http://${address}`; + // Hyrum's Law strikes here: + // Scripts that imported older versions of Firebase Functions SDK accidentally made + // the FIREBASE_CONFIG environment variable always available to the script. + // Many users ended up depending on this behavior, so we conditionally inject the env var + // if the FIREBASE_CONFIG env var isn't explicitly set in the parent process. + if (env.GCLOUD_PROJECT && !env.FIREBASE_CONFIG) { + env.FIREBASE_CONFIG = JSON.stringify({ + projectId: env.GCLOUD_PROJECT, + storageBucket: `${env.GCLOUD_PROJECT}.appspot.com`, + databaseURL: `https://${env.GCLOUD_PROJECT}.firebaseio.com`, + }); } - const authInstance = EmulatorRegistry.get(Emulators.AUTH); - if (authInstance) { - const info = authInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.FIREBASE_AUTH_EMULATOR_HOST] = address; - } - - const hubInstance = EmulatorRegistry.get(Emulators.HUB); - if (hubInstance) { - const info = hubInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.FIREBASE_EMULATOR_HUB] = address; - } + const emulatorInfos = EmulatorRegistry.listRunningWithInfo(); + setEnvVarsForEmulators(env, emulatorInfos); const proc = childProcess.spawn(script, { stdio: ["inherit", "inherit", "inherit"] as childProcess.StdioOptions, @@ -396,32 +416,167 @@ async function runScript(script: string, extraEnv: Record): Prom }); } -/** The action function for emulators:exec and ext:dev:emulators:exec. - * Starts the appropriate emulators, executes the provided script, - * and then exits. - * @param script: A script to run after starting the emulators. - * @param options: A Commander options object. +/** + * For overview tables ONLY. Use EmulatorRegistry methods instead for connecting. + * + * This method returns a string suitable for printing into CLI outputs, resembling + * a netloc part of URL. This makes it clickable in many terminal emulators, a + * specific customer request. + * + * Note that this method does not transform the hostname and may return 0.0.0.0 + * etc. that may not work in some browser / OS combinations. When trying to send + * a network request, use `EmulatorRegistry.client()` instead. When constructing + * URLs (especially links printed/shown), use `EmulatorRegistry.url()`. */ -export async function emulatorExec(script: string, options: any) { - shutdownWhenKilled(options); +export function getListenOverview(emulator: Emulators): string | undefined { + const info = EmulatorRegistry.get(emulator)?.getInfo(); + if (!info) { + return undefined; + } + if (info.host.includes(":")) { + return `[${info.host}]:${info.port}`; + } else { + return `${info.host}:${info.port}`; + } +} + +/** + * The action function for emulators:exec. + * Starts the appropriate emulators, executes the provided script, + * and then exits. + * @param script A script to run after starting the emulators. + * @param options A Commander options object. + */ +export async function emulatorExec(script: string, options: any): Promise { const projectId = getProjectId(options); const extraEnv: Record = {}; if (projectId) { extraEnv.GCLOUD_PROJECT = projectId; } + const session = emulatorSession(); + if (session && session.debugMode) { + // Expose session in debug mode to allow running Emulator UI dev server via: + // firebase emulators:exec 'npm start' + extraEnv[Constants.FIREBASE_GA_SESSION] = JSON.stringify(session); + } let exitCode = 0; + let deprecationNotices; try { const showUI = !!options.ui; - await controller.startAll(options, showUI); + ({ deprecationNotices } = await controller.startAll(options, showUI, true)); exitCode = await runScript(script, extraEnv); - await onExit(options); + await controller.onExit(options); } finally { await controller.cleanShutdown(); } + for (const notice of deprecationNotices) { + utils.logLabeledWarning("emulators", notice, "warn"); + } + if (exitCode !== 0) { throw new FirebaseError(`Script "${clc.bold(script)}" exited with code ${exitCode}`, { exit: exitCode, }); } } + +// Regex to extract Java major version. Only works with Java >= 9. +// See: http://openjdk.java.net/jeps/223 +const JAVA_VERSION_REGEX = /version "([1-9][0-9]*)/; +const JAVA_HINT = "Please make sure Java is installed and on your system PATH."; + +/** + * Return whether Java major verion is supported. Throws if Java not available. + * @return Java major version (for Java >= 9) or -1 otherwise + */ +export async function checkJavaMajorVersion(): Promise { + return new Promise((resolve, reject) => { + let child; + try { + child = childProcess.spawn( + "java", + ["-Duser.language=en", "-Dfile.encoding=UTF-8", "-version"], + { + stdio: ["inherit", "pipe", "pipe"], + }, + ); + } catch (err: any) { + return reject( + new FirebaseError(`Could not spawn \`java -version\`. ${JAVA_HINT}`, { original: err }), + ); + } + + let output = ""; + let error = ""; + child.stdout?.on("data", (data) => { + const str = data.toString("utf8"); + logger.debug(str); + output += str; + }); + child.stderr?.on("data", (data) => { + const str = data.toString("utf8"); + logger.debug(str); + error += str; + }); + + child.once("error", (err) => { + reject( + new FirebaseError(`Could not spawn \`java -version\`. ${JAVA_HINT}`, { original: err }), + ); + }); + + child.once("exit", (code, signal) => { + if (signal) { + // This is an unlikely situation where the short-lived Java process to + // check version was killed by a signal. + reject(new FirebaseError(`Process \`java -version\` was killed by signal ${signal}.`)); + } else if (code && code !== 0) { + // `java -version` failed. For example, this may happen on some OS X + // where `java` is by default a stub that prints out more information on + // how to install Java. It is critical for us to relay stderr/stdout. + reject( + new FirebaseError( + `Process \`java -version\` has exited with code ${code}. ${JAVA_HINT}\n` + + `-----Original stdout-----\n${output}` + + `-----Original stderr-----\n${error}`, + ), + ); + } else { + // Join child process stdout and stderr for further parsing. Order does + // not matter here because we'll parse only a small part later. + resolve(`${output}\n${error}`); + } + }); + }).then((output) => { + let versionInt = -1; + const match = JAVA_VERSION_REGEX.exec(output); + if (match) { + const version = match[1]; + versionInt = parseInt(version, 10); + if (!versionInt) { + utils.logLabeledWarning( + "emulators", + `Failed to parse Java version. Got "${match[0]}".`, + "warn", + ); + } else { + logger.debug(`Parsed Java major version: ${versionInt}`); + } + } else { + // probably Java <= 8 (different version scheme) or unknown + logger.debug("java -version outputs:", output); + logger.warn(`Failed to parse Java version.`); + } + const session = emulatorSession(); + if (session) { + session.javaMajorVersion = versionInt; + } + return versionInt; + }); +} + +export const MIN_SUPPORTED_JAVA_MAJOR_VERSION = 11; +export const JAVA_DEPRECATION_WARNING = + "firebase-tools no longer supports Java version before 11. " + + "Please upgrade to Java version 11 or above to continue using the emulators."; diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index 8813aeacd6f..5731ae2cb72 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -1,31 +1,35 @@ -import * as url from "url"; - import { Emulators } from "./types"; -const DEFAULT_PORTS: { [s in Emulators]: number } = { +export const DEFAULT_PORTS: { [s in Emulators]: number } = { ui: 4000, hub: 4400, logging: 4500, hosting: 5000, functions: 5001, + extensions: 5001, // The Extensions Emulator runs on the same port as the Functions Emulator firestore: 8080, pubsub: 8085, database: 9000, auth: 9099, storage: 9199, + eventarc: 9299, + dataconnect: 9399, }; export const FIND_AVAILBLE_PORT_BY_DEFAULT: Record = { ui: true, hub: true, logging: true, - hosting: false, + hosting: true, functions: false, firestore: false, database: false, pubsub: false, auth: false, storage: false, + extensions: false, + eventarc: true, + dataconnect: true, }; export const EMULATOR_DESCRIPTION: Record = { @@ -39,20 +43,30 @@ export const EMULATOR_DESCRIPTION: Record = { pubsub: "Pub/Sub Emulator", auth: "Authentication Emulator", storage: "Storage Emulator", + extensions: "Extensions Emulator", + eventarc: "Eventarc Emulator", + dataconnect: "Data Connect Emulator", }; -const DEFAULT_HOST = "localhost"; +export const DEFAULT_HOST = "localhost"; export class Constants { // GCP projects cannot start with 'demo' so we use 'demo-' as a prefix to denote // an intentionally fake project. static FAKE_PROJECT_ID_PREFIX = "demo-"; + static FAKE_PROJECT_NUMBER = "0"; static DEFAULT_DATABASE_EMULATOR_NAMESPACE = "fake-server"; + // Environment variable for a list of active CLI experiments + static FIREBASE_ENABLED_EXPERIMENTS = "FIREBASE_ENABLED_EXPERIMENTS"; + // Environment variable to override SDK/CLI to point at the Firestore emulator. static FIRESTORE_EMULATOR_HOST = "FIRESTORE_EMULATOR_HOST"; + // Alternative (deprecated) env var for Firestore Emulator. + static FIRESTORE_EMULATOR_ENV_ALT = "FIREBASE_FIRESTORE_EMULATOR_ADDRESS"; + // Environment variable to override SDK/CLI to point at the Realtime Database emulator. static FIREBASE_DATABASE_EMULATOR_HOST = "FIREBASE_DATABASE_EMULATOR_HOST"; @@ -67,12 +81,20 @@ export class Constants { // this one must start with 'http://'. static CLOUD_STORAGE_EMULATOR_HOST = "STORAGE_EMULATOR_HOST"; + // Environment variable to discover the eventarc emulator. + static PUBSUB_EMULATOR_HOST = "PUBSUB_EMULATOR_HOST"; + + // Environment variable to discover the eventarc emulator. + static CLOUD_EVENTARC_EMULATOR_HOST = "CLOUD_EVENTARC_EMULATOR_HOST"; + // Environment variable to discover the Emulator HUB static FIREBASE_EMULATOR_HUB = "FIREBASE_EMULATOR_HUB"; + static FIREBASE_GA_SESSION = "FIREBASE_GA_SESSION"; static SERVICE_FIRESTORE = "firestore.googleapis.com"; static SERVICE_REALTIME_DATABASE = "firebaseio.com"; static SERVICE_PUBSUB = "pubsub.googleapis.com"; + static SERVICE_EVENTARC = "eventarc.googleapis.com"; // Note: the service name below are here solely for logging purposes. // There is not an emulator available for these. static SERVICE_ANALYTICS = "app-measurement.com"; @@ -102,12 +124,14 @@ export class Constants { return "storage"; case this.SERVICE_TEST_LAB: return "test lab"; + case this.SERVICE_EVENTARC: + return "eventarc"; default: return service; } } - static getDefaultHost(emulator: Emulators): string { + static getDefaultHost(): string { return DEFAULT_HOST; } @@ -119,16 +143,6 @@ export class Constants { return EMULATOR_DESCRIPTION[name]; } - static normalizeHost(host: string): string { - let normalized = host; - if (!normalized.startsWith("http")) { - normalized = `http://${normalized}`; - } - - const u = url.parse(normalized); - return u.hostname || DEFAULT_HOST; - } - static isDemoProject(projectId?: string): boolean { return !!projectId && projectId.startsWith(this.FAKE_PROJECT_ID_PREFIX); } diff --git a/src/emulator/controller.spec.ts b/src/emulator/controller.spec.ts new file mode 100644 index 00000000000..70e782d44eb --- /dev/null +++ b/src/emulator/controller.spec.ts @@ -0,0 +1,22 @@ +import { Emulators } from "./types"; +import { EmulatorRegistry } from "./registry"; +import { expect } from "chai"; +import { FakeEmulator } from "./testing/fakeEmulator"; + +describe("EmulatorController", () => { + afterEach(async () => { + await EmulatorRegistry.stopAll(); + }); + + it("should start and stop an emulator", async () => { + const name = Emulators.FUNCTIONS; + + expect(EmulatorRegistry.isRunning(name)).to.be.false; + + const fake = await FakeEmulator.create(name); + await EmulatorRegistry.start(fake); + + expect(EmulatorRegistry.isRunning(name)).to.be.true; + expect(EmulatorRegistry.getInfo(name)!.port).to.eql(fake.getInfo().port); + }); +}); diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts old mode 100644 new mode 100755 index 9dcc89c9ec5..4ebbea9f94e --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -1,136 +1,68 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import * as path from "path"; +import * as fsConfig from "../firestore/fsConfig"; -import { Config } from "../config"; import { logger } from "../logger"; -import * as track from "../track"; +import { trackEmulator, trackGA4 } from "../track"; import * as utils from "../utils"; import { EmulatorRegistry } from "./registry"; import { - Address, + ALL_EMULATORS, ALL_SERVICE_EMULATORS, + EmulatorInfo, EmulatorInstance, Emulators, EMULATORS_SUPPORTED_BY_UI, isEmulator, } from "./types"; import { Constants, FIND_AVAILBLE_PORT_BY_DEFAULT } from "./constants"; -import { FunctionsEmulator } from "./functionsEmulator"; -import { parseRuntimeVersion } from "./functionsEmulatorUtils"; -import { AuthEmulator } from "./auth"; -import { DatabaseEmulator, DatabaseEmulatorArgs } from "./databaseEmulator"; -import { FirestoreEmulator, FirestoreEmulatorArgs } from "./firestoreEmulator"; -import { HostingEmulator } from "./hostingEmulator"; +import { EmulatableBackend, FunctionsEmulator } from "./functionsEmulator"; import { FirebaseError } from "../error"; -import { getProjectId, needProjectId } from "../projectUtils"; -import { PubsubEmulator } from "./pubsubEmulator"; +import { getProjectId, needProjectId, getAliases, needProjectNumber } from "../projectUtils"; import * as commandUtils from "./commandUtils"; import { EmulatorHub } from "./hub"; import { ExportMetadata, HubExport } from "./hubExport"; import { EmulatorUI } from "./ui"; import { LoggingEmulator } from "./loggingEmulator"; import * as dbRulesConfig from "../database/rulesConfig"; -import { EmulatorLogger } from "./emulatorLogger"; -import * as portUtils from "./portUtils"; +import { EmulatorLogger, Verbosity } from "./emulatorLogger"; import { EmulatorHubClient } from "./hubClient"; -import { promptOnce } from "../prompt"; -import { FLAG_EXPORT_ON_EXIT_NAME } from "./commandUtils"; +import { confirm } from "../prompt"; +import { + FLAG_EXPORT_ON_EXIT_NAME, + JAVA_DEPRECATION_WARNING, + MIN_SUPPORTED_JAVA_MAJOR_VERSION, +} from "./commandUtils"; import { fileExistsSync } from "../fsutils"; -import { StorageEmulator } from "./storage"; +import { getStorageRulesConfig } from "./storage/rules/config"; import { getDefaultDatabaseInstance } from "../getDefaultDatabaseInstance"; import { getProjectDefaultAccount } from "../auth"; import { Options } from "../options"; import { ParsedTriggerDefinition } from "./functionsEmulatorShared"; +import { ExtensionsEmulator } from "./extensionsEmulator"; +import { normalizeAndValidate } from "../functions/projectConfig"; +import { requiresJava } from "./downloadableEmulators"; +import { prepareFrameworks } from "../frameworks"; +import * as experiments from "../experiments"; +import { EmulatorListenConfig, PortName, resolveHostAndAssignPorts } from "./portUtils"; +import { Runtime, isRuntime } from "../deploy/functions/runtimes/supported"; + +import { AuthEmulator, SingleProjectMode } from "./auth"; +import { DatabaseEmulator, DatabaseEmulatorArgs } from "./databaseEmulator"; +import { EventarcEmulator } from "./eventarcEmulator"; +import { DataConnectEmulator } from "./dataconnectEmulator"; +import { FirestoreEmulator, FirestoreEmulatorArgs } from "./firestoreEmulator"; +import { HostingEmulator } from "./hostingEmulator"; +import { PubsubEmulator } from "./pubsubEmulator"; +import { StorageEmulator } from "./storage"; +import { readFirebaseJson } from "../dataconnect/fileUtils"; -async function getAndCheckAddress(emulator: Emulators, options: Options): Promise
    { - let host = options.config.src.emulators?.[emulator]?.host || Constants.getDefaultHost(emulator); - if (host === "localhost" && utils.isRunningInWSL()) { - // HACK(https://github.com/firebase/firebase-tools-ui/issues/332): Use IPv4 - // 127.0.0.1 instead of localhost. This, combined with the hack in - // downloadableEmulators.ts, forces the emulator to listen on IPv4 ONLY. - // The CLI (including the hub) will also consistently report 127.0.0.1, - // causing clients to connect via IPv4 only (which mitigates the problem of - // some clients resolving localhost to IPv6 and get connection refused). - host = "127.0.0.1"; - } - - const portVal = options.config.src.emulators?.[emulator]?.port; - let port; - let findAvailablePort = false; - if (portVal) { - port = parseInt(`${portVal}`, 10); - } else { - port = Constants.getDefaultPort(emulator); - findAvailablePort = FIND_AVAILBLE_PORT_BY_DEFAULT[emulator]; - } - - const loggerForEmulator = EmulatorLogger.forEmulator(emulator); - const portOpen = await portUtils.checkPortOpen(port, host); - if (!portOpen) { - if (findAvailablePort) { - const newPort = await portUtils.findAvailablePort(host, port); - if (newPort != port) { - loggerForEmulator.logLabeled( - "WARN", - emulator, - `${Constants.description( - emulator - )} unable to start on port ${port}, starting on ${newPort} instead.` - ); - port = newPort; - } - } else { - await cleanShutdown(); - const description = Constants.description(emulator); - loggerForEmulator.logLabeled( - "WARN", - emulator, - `Port ${port} is not open on ${host}, could not start ${description}.` - ); - loggerForEmulator.logLabeled( - "WARN", - emulator, - `To select a different host/port, specify that host/port in a firebase.json config file: - { - // ... - "emulators": { - "${emulator}": { - "host": "${clc.yellow("HOST")}", - "port": "${clc.yellow("PORT")}" - } - } - }` - ); - return utils.reject(`Could not start ${description}, port taken.`, {}); - } - } - - if (portUtils.isRestricted(port)) { - const suggested = portUtils.suggestUnrestricted(port); - loggerForEmulator.logLabeled( - "WARN", - emulator, - `Port ${port} is restricted by some web browsers, including Chrome. You may want to choose a different port such as ${suggested}.` - ); - } - - return { host, port }; -} - -/** - * Starts a specific emulator instance - * @param instance - */ -export async function startEmulator(instance: EmulatorInstance): Promise { - const name = instance.getName(); - - // Log the command for analytics - track("Emulator Run", name); - - await EmulatorRegistry.start(instance); -} +const START_LOGGING_EMULATOR = utils.envOverride( + "START_LOGGING_EMULATOR", + "false", + (val) => val === "true", +); /** * Exports emulator data on clean exit (SIGINT or process end) @@ -142,10 +74,10 @@ export async function exportOnExit(options: any) { try { utils.logBullet( `Automatically exporting data using ${FLAG_EXPORT_ON_EXIT_NAME} "${exportOnExitDir}" ` + - "please wait for the export to finish..." + "please wait for the export to finish...", ); - await exportEmulatorData(exportOnExitDir, options); - } catch (e) { + await exportEmulatorData(exportOnExitDir, options, /* initiatedBy= */ "exit"); + } catch (e: any) { utils.logWarning(e); utils.logWarning(`Automatic export to "${exportOnExitDir}" failed, going to exit now...`); } @@ -168,7 +100,7 @@ export async function cleanShutdown(): Promise { EmulatorLogger.forEmulator(Emulators.HUB).logLabeled( "BULLET", "emulators", - "Shutting down emulators." + "Shutting down emulators.", ); await EmulatorRegistry.stopAll(); } @@ -177,8 +109,10 @@ export async function cleanShutdown(): Promise { * Filters a list of emulators to only those specified in the config * @param options */ -export function filterEmulatorTargets(options: any): Emulators[] { - let targets = ALL_SERVICE_EMULATORS.filter((e) => { +export function filterEmulatorTargets(options: { only: string; config: any }): Emulators[] { + let targets = [...ALL_SERVICE_EMULATORS]; + targets.push(Emulators.EXTENSIONS); + targets = targets.filter((e) => { return options.config.has(e) || options.config.has(`emulators.${e}`); }); @@ -187,7 +121,7 @@ export function filterEmulatorTargets(options: any): Emulators[] { const only = onlyOptions.split(",").map((o) => { return o.split(":")[0]; }); - targets = _.intersection(targets, only as Emulators[]); + targets = targets.filter((t) => only.includes(t)); } return targets; @@ -224,15 +158,20 @@ export function shouldStart(options: Options, name: Emulators): boolean { } // Don't start the functions emulator if we can't find the source directory - if (name === Emulators.FUNCTIONS && emulatorInTargets && !options.config.src.functions?.source) { - EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled( - "WARN", - "functions", - `The functions emulator is configured but there is no functions source directory. Have you run ${clc.bold( - "firebase init functions" - )}?` - ); - return false; + if (name === Emulators.FUNCTIONS && emulatorInTargets) { + try { + normalizeAndValidate(options.config.src.functions); + return true; + } catch (err: any) { + EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled( + "WARN", + "functions", + `The functions emulator is configured but there is no functions source directory. Have you run ${clc.bold( + "firebase init functions", + )}?`, + ); + return false; + } } if (name === Emulators.HOSTING && emulatorInTargets && !options.config.get("hosting")) { @@ -240,8 +179,8 @@ export function shouldStart(options: Options, name: Emulators): boolean { "WARN", "hosting", `The hosting emulator is configured but there is no hosting configuration. Have you run ${clc.bold( - "firebase init hosting" - )}?` + "firebase init hosting", + )}?`, ); return false; } @@ -250,6 +189,11 @@ export function shouldStart(options: Options, name: Emulators): boolean { } function findExportMetadata(importPath: string): ExportMetadata | undefined { + const pathExists = fs.existsSync(importPath); + if (!pathExists) { + throw new FirebaseError(`Directory "${importPath}" does not exist.`); + } + const pathIsDirectory = fs.lstatSync(importPath).isDirectory(); if (!pathIsDirectory) { return; @@ -278,7 +222,7 @@ function findExportMetadata(importPath: string): ExportMetadata | undefined { EmulatorLogger.forEmulator(Emulators.FIRESTORE).logLabeled( "BULLET", "firestore", - `Detected non-emulator Firestore export at ${importPath}` + `Detected non-emulator Firestore export at ${importPath}`, ); return metadata; @@ -298,14 +242,26 @@ function findExportMetadata(importPath: string): ExportMetadata | undefined { EmulatorLogger.forEmulator(Emulators.DATABASE).logLabeled( "BULLET", "firestore", - `Detected non-emulator Database export at ${importPath}` + `Detected non-emulator Database export at ${importPath}`, ); return metadata; } } -export async function startAll(options: Options, showUI: boolean = true): Promise { +interface EmulatorOptions extends Options { + extDevEnv?: Record; + logVerbosity?: "DEBUG" | "INFO" | "QUIET" | "SILENT"; +} + +/** + * Start all emulators. + */ +export async function startAll( + options: EmulatorOptions, + showUI = true, + runningTestScript = false, +): Promise<{ deprecationNotices: string[] }> { // Emulators config is specified in firebase.json as: // "emulators": { // "firestore": { @@ -320,21 +276,35 @@ export async function startAll(options: Options, showUI: boolean = true): Promis // 2) If the --only flag is passed, then this list is the intersection const targets = filterEmulatorTargets(options); options.targets = targets; + const singleProjectModeEnabled = + options.config.src.emulators?.singleProjectMode === undefined || + options.config.src.emulators?.singleProjectMode; if (targets.length === 0) { throw new FirebaseError( - `No emulators to start, run ${clc.bold("firebase init emulators")} to get started.` + `No emulators to start, run ${clc.bold("firebase init emulators")} to get started.`, ); } + if (targets.some(requiresJava)) { + if ((await commandUtils.checkJavaMajorVersion()) < MIN_SUPPORTED_JAVA_MAJOR_VERSION) { + utils.logLabeledError("emulators", JAVA_DEPRECATION_WARNING, "warn"); + throw new FirebaseError(JAVA_DEPRECATION_WARNING); + } + } + if (options.logVerbosity) { + EmulatorLogger.setVerbosity(Verbosity[options.logVerbosity]); + } + const hubLogger = EmulatorLogger.forEmulator(Emulators.HUB); hubLogger.logLabeled("BULLET", "emulators", `Starting emulators: ${targets.join(", ")}`); const projectId: string = getProjectId(options) || ""; // TODO: Next breaking change, consider making this fall back to demo project. - if (Constants.isDemoProject(projectId)) { + const isDemoProject = Constants.isDemoProject(projectId); + if (isDemoProject) { hubLogger.logLabeled( "BULLET", "emulators", - `Detected demo project ID "${projectId}", emulated services will use a demo configuration and attempts to access non-emulated services for this project will fail.` + `Detected demo project ID "${projectId}", emulated services will use a demo configuration and attempts to access non-emulated services for this project will fail.`, ); } @@ -343,7 +313,7 @@ export async function startAll(options: Options, showUI: boolean = true): Promis const requested: string[] = onlyOptions.split(",").map((o) => { return o.split(":")[0]; }); - const ignored = _.difference(requested, targets); + const ignored = requested.filter((k) => !targets.includes(k as Emulators)); for (const name of ignored) { if (isEmulator(name)) { @@ -351,31 +321,118 @@ export async function startAll(options: Options, showUI: boolean = true): Promis "WARN", name, `Not starting the ${clc.bold(name)} emulator, make sure you have run ${clc.bold( - "firebase init" - )}.` + "firebase init", + )}.`, ); } else { // this should not work: - // firebase emulators:start --only doesnotexit + // firebase emulators:start --only doesnotexist throw new FirebaseError( `${name} is not a valid emulator name, valid options are: ${JSON.stringify( - ALL_SERVICE_EMULATORS + ALL_SERVICE_EMULATORS, )}`, - { exit: 1 } + { exit: 1 }, ); } } } - if (shouldStart(options, Emulators.HUB)) { - const hubAddr = await getAndCheckAddress(Emulators.HUB, options); - const hub = new EmulatorHub({ projectId, ...hubAddr }); + const emulatableBackends: EmulatableBackend[] = []; + + // Process extensions config early so that we have a better guess at whether + // the Functions emulator needs to start. + let extensionEmulator: ExtensionsEmulator | undefined = undefined; + if (shouldStart(options, Emulators.EXTENSIONS)) { + const projectNumber = isDemoProject + ? Constants.FAKE_PROJECT_NUMBER + : await needProjectNumber(options); + const aliases = getAliases(options, projectId); + extensionEmulator = new ExtensionsEmulator({ + projectId, + projectDir: options.config.projectDir, + projectNumber, + aliases, + extensions: options.config.get("extensions"), + }); + const extensionsBackends = await extensionEmulator.getExtensionBackends(); + const filteredExtensionsBackends = extensionEmulator.filterUnemulatedTriggers( + options, + extensionsBackends, + ); + emulatableBackends.push(...filteredExtensionsBackends); + trackGA4("extensions_emulated", { + number_of_extensions_emulated: filteredExtensionsBackends.length, + number_of_extensions_ignored: extensionsBackends.length - filteredExtensionsBackends.length, + }); + } + + const listenConfig = {} as Record; + if (emulatableBackends.length) { + // If we already know we need Functions (and Eventarc), assign them now. + listenConfig[Emulators.FUNCTIONS] = getListenConfig(options, Emulators.FUNCTIONS); + listenConfig[Emulators.EVENTARC] = getListenConfig(options, Emulators.EVENTARC); + } + for (const emulator of ALL_EMULATORS) { + if ( + emulator === Emulators.FUNCTIONS || + emulator === Emulators.EVENTARC || + // Same port as Functions, no need for separate assignment + emulator === Emulators.EXTENSIONS || + (emulator === Emulators.UI && !showUI) + ) { + continue; + } + if ( + shouldStart(options, emulator) || + (emulator === Emulators.LOGGING && + ((showUI && shouldStart(options, Emulators.UI)) || START_LOGGING_EMULATOR)) + ) { + const config = getListenConfig(options, emulator); + listenConfig[emulator] = config; + if (emulator === Emulators.FIRESTORE) { + const wsPortConfig = options.config.src.emulators?.firestore?.websocketPort; + listenConfig["firestore.websocket"] = { + host: config.host, + port: wsPortConfig || 9150, + portFixed: !!wsPortConfig, + }; + } + } + } + let listenForEmulator = await resolveHostAndAssignPorts(listenConfig); + hubLogger.log("DEBUG", "assigned listening specs for emulators", { user: listenForEmulator }); + + function legacyGetFirstAddr(name: PortName): { host: string; port: number } { + const firstSpec = listenForEmulator[name][0]; + return { + host: firstSpec.address, + port: firstSpec.port, + }; + } + + function startEmulator(instance: EmulatorInstance): Promise { + const name = instance.getName(); + + // Log the command for analytics + void trackEmulator("emulator_run", { + emulator_name: name, + is_demo_project: String(isDemoProject), + }); + + return EmulatorRegistry.start(instance); + } + + if (listenForEmulator.hub) { + const hub = new EmulatorHub({ + projectId, + listen: listenForEmulator[Emulators.HUB], + listenForEmulator, + }); // Log the command for analytics, we only report this for "hub" // since we originally mistakenly reported emulators:start events // for each emulator, by reporting the "hub" we ensure that our // historical data can still be viewed. - track("emulators:start", "hub"); await startEmulator(hub); } @@ -389,85 +446,159 @@ export async function startAll(options: Options, showUI: boolean = true): Promis const foundMetadata = findExportMetadata(importDir); if (foundMetadata) { exportMetadata = foundMetadata; + void trackEmulator("emulator_import", { + initiated_by: "start", + emulator_name: Emulators.HUB, + }); } else { hubLogger.logLabeled( "WARN", "emulators", - `Could not find import/export metadata file, ${clc.bold("skipping data import!")}` + `Could not find import/export metadata file, ${clc.bold("skipping data import!")}`, ); } } - if (shouldStart(options, Emulators.FUNCTIONS)) { - const functionsLogger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); - const functionsAddr = await getAndCheckAddress(Emulators.FUNCTIONS, options); - const projectId = needProjectId(options); - - utils.assertDefined(options.config.src.functions); - utils.assertDefined( - options.config.src.functions.source, - "Error: 'functions.source' is not defined" + // TODO: turn this into hostingConfig.extract or hostingConfig.hostingConfig + // once those branches merge + const hostingConfig = options.config.get("hosting"); + if ( + Array.isArray(hostingConfig) ? hostingConfig.some((it) => it.source) : hostingConfig?.source + ) { + experiments.assertEnabled("webframeworks", "emulate a web framework"); + const emulators: EmulatorInfo[] = []; + for (const e of ALL_SERVICE_EMULATORS) { + // TODO(yuchenshi): Functions and Eventarc may be missing if they are not + // yet known to be needed and then prepareFrameworks adds extra functions. + if (listenForEmulator[e]) { + emulators.push({ + name: e, + host: utils.connectableHostname(listenForEmulator[e][0].address), + port: listenForEmulator[e][0].port, + }); + } + } + // This may add additional sources for Functions emulator and must be done before it. + await prepareFrameworks( + runningTestScript ? "test" : "emulate", + targets, + undefined, + options, + emulators, ); + } - utils.assertIsStringOrUndefined(options.extensionDir); - const functionsDir = path.join( - options.extensionDir || options.config.projectDir, - options.config.src.functions.source - ); + const projectDir = (options.extDevDir || options.config.projectDir) as string; + if (shouldStart(options, Emulators.FUNCTIONS)) { + const functionsCfg = normalizeAndValidate(options.config.src.functions); + // Note: ext:dev:emulators:* commands hit this path, not the Emulators.EXTENSIONS path + utils.assertIsStringOrUndefined(options.extDevDir); + + for (const cfg of functionsCfg) { + const functionsDir = path.join(projectDir, cfg.source); + const runtime = (options.extDevRuntime ?? cfg.runtime) as Runtime | undefined; + // N.B. (Issue #6965) it's OK for runtime to be undefined because the functions discovery process + // will dynamically detect it later. + // TODO: does runtime even need to be a part of EmultableBackend now that we have dynamic runtime + // detection? Might be an extensions thing. + if (runtime && !isRuntime(runtime)) { + throw new FirebaseError( + `Cannot load functions from ${functionsDir} because it has invalid runtime ${runtime as string}`, + ); + } + emulatableBackends.push({ + functionsDir, + runtime, + codebase: cfg.codebase, + env: { + ...options.extDevEnv, + }, + secretEnv: [], // CF3 secrets are bound to specific functions, so we'll get them during trigger discovery. + // TODO(b/213335255): predefinedTriggers and nodeMajorVersion are here to support ext:dev:emulators:* commands. + // Ideally, we should handle that case via ExtensionEmulator. + predefinedTriggers: options.extDevTriggers as ParsedTriggerDefinition[] | undefined, + }); + } + } - let inspectFunctions: number | undefined; - if (options.inspectFunctions) { - inspectFunctions = commandUtils.parseInspectionPort(options); + if (extensionEmulator) { + await startEmulator(extensionEmulator); + } + + if (emulatableBackends.length) { + if (!listenForEmulator.functions || !listenForEmulator.eventarc) { + // We did not know that we need Functions and Eventarc earlier but now we do. + listenForEmulator = await resolveHostAndAssignPorts({ + ...listenForEmulator, + functions: listenForEmulator.functions ?? getListenConfig(options, Emulators.FUNCTIONS), + eventarc: listenForEmulator.eventarc ?? getListenConfig(options, Emulators.EVENTARC), + }); + hubLogger.log("DEBUG", "late-assigned ports for functions and eventarc emulators", { + user: listenForEmulator, + }); + } + const functionsLogger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); + const functionsAddr = legacyGetFirstAddr(Emulators.FUNCTIONS); + const projectId = needProjectId(options); + const inspectFunctions = commandUtils.parseInspectionPort(options); + if (inspectFunctions) { // TODO(samstern): Add a link to documentation functionsLogger.logLabeled( "WARN", "functions", - `You are running the functions emulator in debug mode (port=${inspectFunctions}). This means that functions will execute in sequence rather than in parallel.` + `You are running the Functions emulator in debug mode. This means that functions will execute in sequence rather than in parallel.`, ); } - // Warn the developer that the Functions emulator can call out to production. + // Warn the developer that the Functions/Extensions emulator can call out to production. const emulatorsNotRunning = ALL_SERVICE_EMULATORS.filter((e) => { - return e !== Emulators.FUNCTIONS && !shouldStart(options, e); + return e !== Emulators.FUNCTIONS && !listenForEmulator[e]; }); if (emulatorsNotRunning.length > 0 && !Constants.isDemoProject(projectId)) { functionsLogger.logLabeled( "WARN", "functions", `The following emulators are not running, calls to these services from the Functions emulator will affect production: ${clc.bold( - emulatorsNotRunning.join(", ") - )}` + emulatorsNotRunning.join(", "), + )}`, ); } - const account = getProjectDefaultAccount(options.projectRoot as string | null); + const account = getProjectDefaultAccount(options.projectRoot); + + // TODO(b/213241033): Figure out how to watch for changes to extensions .env files & reload triggers when they change. const functionsEmulator = new FunctionsEmulator({ projectId, - functionsDir, + projectDir, + emulatableBackends, account, host: functionsAddr.host, port: functionsAddr.port, debugPort: inspectFunctions, - env: { - ...(options.extensionEnv as Record | undefined), - }, - predefinedTriggers: options.extensionTriggers as ParsedTriggerDefinition[] | undefined, - nodeMajorVersion: parseRuntimeVersion( - options.extensionNodeVersion || options.config.get("functions.runtime") - ), + verbosity: options.logVerbosity, + projectAlias: options.projectAlias, }); await startEmulator(functionsEmulator); + + const eventarcAddr = legacyGetFirstAddr(Emulators.EVENTARC); + const eventarcEmulator = new EventarcEmulator({ + host: eventarcAddr.host, + port: eventarcAddr.port, + }); + await startEmulator(eventarcEmulator); } - if (shouldStart(options, Emulators.FIRESTORE)) { + if (listenForEmulator.firestore) { const firestoreLogger = EmulatorLogger.forEmulator(Emulators.FIRESTORE); - const firestoreAddr = await getAndCheckAddress(Emulators.FIRESTORE, options); + const firestoreAddr = legacyGetFirstAddr(Emulators.FIRESTORE); + const websocketPort = legacyGetFirstAddr("firestore.websocket").port; const args: FirestoreEmulatorArgs = { host: firestoreAddr.host, port: firestoreAddr.port, - projectId, + websocket_port: websocketPort, + project_id: projectId, auto_download: true, }; @@ -476,20 +607,45 @@ export async function startAll(options: Options, showUI: boolean = true): Promis const importDirAbsPath = path.resolve(options.import); const exportMetadataFilePath = path.resolve( importDirAbsPath, - exportMetadata.firestore.metadata_file + exportMetadata.firestore.metadata_file, ); firestoreLogger.logLabeled( "BULLET", "firestore", - `Importing data from ${exportMetadataFilePath}` + `Importing data from ${exportMetadataFilePath}`, ); args.seed_from_export = exportMetadataFilePath; + void trackEmulator("emulator_import", { + initiated_by: "start", + emulator_name: Emulators.FIRESTORE, + }); } const config = options.config; - const rulesLocalPath = config.src.firestore?.rules; - let rulesFileFound = false; + // emulator does not support multiple databases yet + // TODO(VicVer09): b/269787702 + let rulesLocalPath; + let rulesFileFound; + const firestoreConfigs: fsConfig.ParsedFirestoreConfig[] = fsConfig.getFirestoreConfig( + projectId, + options, + ); + if (!firestoreConfigs) { + firestoreLogger.logLabeled( + "WARN", + "firestore", + `Cloud Firestore config does not exist in firebase.json.`, + ); + } else if (firestoreConfigs.length !== 1) { + firestoreLogger.logLabeled( + "WARN", + "firestore", + `Cloud Firestore Emulator does not support multiple databases yet.`, + ); + } else if (firestoreConfigs[0].rules) { + rulesLocalPath = firestoreConfigs[0].rules; + } if (rulesLocalPath) { const rules: string = config.path(rulesLocalPath); rulesFileFound = fs.existsSync(rules); @@ -499,14 +655,14 @@ export async function startAll(options: Options, showUI: boolean = true): Promis firestoreLogger.logLabeled( "WARN", "firestore", - `Cloud Firestore rules file ${clc.bold(rules)} specified in firebase.json does not exist.` + `Cloud Firestore rules file ${clc.bold(rules)} specified in firebase.json does not exist.`, ); } } else { firestoreLogger.logLabeled( "WARN", "firestore", - "Did not find a Cloud Firestore rules file specified in a firebase.json config file." + "Did not find a Cloud Firestore rules file specified in a firebase.json config file.", ); } @@ -514,23 +670,44 @@ export async function startAll(options: Options, showUI: boolean = true): Promis firestoreLogger.logLabeled( "WARN", "firestore", - "The emulator will default to allowing all reads and writes. Learn more about this option: https://firebase.google.com/docs/emulator-suite/install_and_configure#security_rules_configuration." + "The emulator will default to allowing all reads and writes. Learn more about this option: https://firebase.google.com/docs/emulator-suite/install_and_configure#security_rules_configuration.", ); } + // undefined in the config defaults to setting single_project_mode. + if (singleProjectModeEnabled) { + if (projectId) { + args.single_project_mode = true; + args.single_project_mode_error = false; + } else { + firestoreLogger.logLabeled( + "DEBUG", + "firestore", + "Could not enable single_project_mode: missing projectId.", + ); + } + } + const firestoreEmulator = new FirestoreEmulator(args); await startEmulator(firestoreEmulator); + firestoreLogger.logLabeled( + "SUCCESS", + Emulators.FIRESTORE, + `Firestore Emulator UI websocket is running on ${websocketPort}.`, + ); } - if (shouldStart(options, Emulators.DATABASE)) { + if (listenForEmulator.database) { const databaseLogger = EmulatorLogger.forEmulator(Emulators.DATABASE); - const databaseAddr = await getAndCheckAddress(Emulators.DATABASE, options); + const databaseAddr = legacyGetFirstAddr(Emulators.DATABASE); const args: DatabaseEmulatorArgs = { host: databaseAddr.host, port: databaseAddr.port, projectId, auto_download: true, + // Only set the flag (at all) if singleProjectMode is enabled. + single_project_mode: singleProjectModeEnabled ? "Warning" : undefined, }; // Try to fetch the default RTDB instance for a project, but don't hard-fail if we @@ -539,16 +716,16 @@ export async function startAll(options: Options, showUI: boolean = true): Promis if (!options.instance) { options.instance = await getDefaultDatabaseInstance(options); } - } catch (e) { + } catch (e: any) { databaseLogger.log( "DEBUG", - `Failed to retrieve default database instance: ${JSON.stringify(e)}` + `Failed to retrieve default database instance: ${JSON.stringify(e)}`, ); } const rc = dbRulesConfig.normalizeRulesConfig( dbRulesConfig.getRulesConfig(projectId, options), - options + options, ); logger.debug("database rules config: ", JSON.stringify(rc)); @@ -558,7 +735,7 @@ export async function startAll(options: Options, showUI: boolean = true): Promis databaseLogger.logLabeled( "WARN", "database", - "Did not find a Realtime Database rules file specified in a firebase.json config file. The emulator will default to allowing all reads and writes. Learn more about this option: https://firebase.google.com/docs/emulator-suite/install_and_configure#security_rules_configuration." + "Did not find a Realtime Database rules file specified in a firebase.json config file. The emulator will default to allowing all reads and writes. Learn more about this option: https://firebase.google.com/docs/emulator-suite/install_and_configure#security_rules_configuration.", ); } else { for (const c of rc) { @@ -568,8 +745,8 @@ export async function startAll(options: Options, showUI: boolean = true): Promis "WARN", "database", `Realtime Database rules file ${clc.bold( - rules - )} specified in firebase.json does not exist.` + rules, + )} specified in firebase.json does not exist.`, ); } } @@ -584,6 +761,11 @@ export async function startAll(options: Options, showUI: boolean = true): Promis const databaseExportDir = path.resolve(importDirAbsPath, exportMetadata.database.path); const files = fs.readdirSync(databaseExportDir).filter((f) => f.endsWith(".json")); + void trackEmulator("emulator_import", { + initiated_by: "start", + emulator_name: Emulators.DATABASE, + count: files.length, + }); for (const f of files) { const fPath = path.join(databaseExportDir, f); const ns = path.basename(f, ".json"); @@ -592,20 +774,23 @@ export async function startAll(options: Options, showUI: boolean = true): Promis } } - if (shouldStart(options, Emulators.AUTH)) { + if (listenForEmulator.auth) { if (!projectId) { throw new FirebaseError( `Cannot start the ${Constants.description( - Emulators.AUTH - )} without a project: run 'firebase init' or provide the --project flag` + Emulators.AUTH, + )} without a project: run 'firebase init' or provide the --project flag`, ); } - const authAddr = await getAndCheckAddress(Emulators.AUTH, options); + const authAddr = legacyGetFirstAddr(Emulators.AUTH); const authEmulator = new AuthEmulator({ host: authAddr.host, port: authAddr.port, projectId, + singleProjectMode: singleProjectModeEnabled + ? SingleProjectMode.WARNING + : SingleProjectMode.NO_WARNING, }); await startEmulator(authEmulator); @@ -614,18 +799,18 @@ export async function startAll(options: Options, showUI: boolean = true): Promis const importDirAbsPath = path.resolve(options.import); const authExportDir = path.resolve(importDirAbsPath, exportMetadata.auth.path); - await authEmulator.importData(authExportDir, projectId); + await authEmulator.importData(authExportDir, projectId, { initiatedBy: "start" }); } } - if (shouldStart(options, Emulators.PUBSUB)) { + if (listenForEmulator.pubsub) { if (!projectId) { throw new FirebaseError( - "Cannot start the Pub/Sub emulator without a project: run 'firebase init' or provide the --project flag" + "Cannot start the Pub/Sub emulator without a project: run 'firebase init' or provide the --project flag", ); } - const pubsubAddr = await getAndCheckAddress(Emulators.PUBSUB, options); + const pubsubAddr = legacyGetFirstAddr(Emulators.PUBSUB); const pubsubEmulator = new PubsubEmulator({ host: pubsubAddr.host, port: pubsubAddr.port, @@ -635,21 +820,38 @@ export async function startAll(options: Options, showUI: boolean = true): Promis await startEmulator(pubsubEmulator); } - if (shouldStart(options, Emulators.STORAGE)) { - const storageAddr = await getAndCheckAddress(Emulators.STORAGE, options); - const storageConfig = options.config.data.storage; - - if (!storageConfig?.rules) { - throw new FirebaseError( - "Cannot start the Storage emulator without rules file specified in firebase.json: run 'firebase init' and set up your Storage configuration" + if (listenForEmulator.dataconnect) { + const config = readFirebaseJson(options.config); + if (!config.length) { + throw new FirebaseError("No Data Connect service found in firebase.json"); + } else if (config.length > 1) { + logger.warn( + `TODO: Add support for multiple services in the Data Connect emulator. Currently emulating first service ${config[0].source}`, ); } + let configDir = config[0].source; + if (!path.isAbsolute(configDir)) { + const cwd = options.cwd || process.cwd(); + configDir = path.resolve(path.join(cwd), configDir); + } + const dataConnectEmulator = new DataConnectEmulator({ + listen: listenForEmulator.dataconnect, + projectId, + auto_download: true, + configDir, + rc: options.rc, + }); + await startEmulator(dataConnectEmulator); + } + + if (listenForEmulator.storage) { + const storageAddr = legacyGetFirstAddr(Emulators.STORAGE); const storageEmulator = new StorageEmulator({ host: storageAddr.host, port: storageAddr.port, projectId: projectId, - rules: options.config.path(storageConfig.rules), + rules: getStorageRulesConfig(projectId, options), }); await startEmulator(storageEmulator); @@ -657,14 +859,14 @@ export async function startAll(options: Options, showUI: boolean = true): Promis utils.assertIsString(options.import); const importDirAbsPath = path.resolve(options.import); const storageExportDir = path.resolve(importDirAbsPath, exportMetadata.storage.path); - storageEmulator.storageLayer.import(storageExportDir); + storageEmulator.storageLayer.import(storageExportDir, { initiatedBy: "start" }); } } // Hosting emulator needs to start after all of the others so that we can detect // which are running and call useEmulator in __init.js - if (shouldStart(options, Emulators.HOSTING)) { - const hostingAddr = await getAndCheckAddress(Emulators.HOSTING, options); + if (listenForEmulator.hosting) { + const hostingAddr = legacyGetFirstAddr(Emulators.HOSTING); const hostingEmulator = new HostingEmulator({ host: hostingAddr.host, port: hostingAddr.port, @@ -674,39 +876,86 @@ export async function startAll(options: Options, showUI: boolean = true): Promis await startEmulator(hostingEmulator); } - if (showUI && !shouldStart(options, Emulators.UI)) { - hubLogger.logLabeled( - "WARN", - "emulators", - "The Emulator UI requires a project ID to start. Configure your default project with 'firebase use' or pass the --project flag." - ); - } - - if (showUI && shouldStart(options, Emulators.UI)) { - const loggingAddr = await getAndCheckAddress(Emulators.LOGGING, options); + if (listenForEmulator.logging) { + const loggingAddr = legacyGetFirstAddr(Emulators.LOGGING); const loggingEmulator = new LoggingEmulator({ host: loggingAddr.host, port: loggingAddr.port, }); await startEmulator(loggingEmulator); + } - const uiAddr = await getAndCheckAddress(Emulators.UI, options); + if (showUI && !shouldStart(options, Emulators.UI)) { + hubLogger.logLabeled( + "WARN", + "emulators", + "The Emulator UI is not starting, either because none of the running " + + "emulators have a UI component or the Emulator UI cannot " + + "determine the Project ID. Pass the --project flag to specify a project.", + ); + } + + if (listenForEmulator.ui) { const ui = new EmulatorUI({ projectId: projectId, auto_download: true, - ...uiAddr, + listen: listenForEmulator[Emulators.UI], }); await startEmulator(ui); } + let serviceEmulatorCount = 0; const running = EmulatorRegistry.listRunning(); for (const name of running) { const instance = EmulatorRegistry.get(name); if (instance) { await instance.connect(); } + if (ALL_SERVICE_EMULATORS.includes(name)) { + serviceEmulatorCount++; + } } + + void trackEmulator("emulators_started", { + count: serviceEmulatorCount, + count_all: running.length, + is_demo_project: String(isDemoProject), + }); + + return { deprecationNotices: [] }; +} + +function getListenConfig( + options: EmulatorOptions, + emulator: Exclude, +): EmulatorListenConfig { + let host = options.config.src.emulators?.[emulator]?.host || Constants.getDefaultHost(); + if (host === "localhost" && utils.isRunningInWSL()) { + // HACK(https://github.com/firebase/firebase-tools-ui/issues/332): Use IPv4 + // 127.0.0.1 instead of localhost. This, combined with the hack in + // downloadableEmulators.ts, forces the emulator to listen on IPv4 ONLY. + // The CLI (including the hub) will also consistently report 127.0.0.1, + // causing clients to connect via IPv4 only (which mitigates the problem of + // some clients resolving localhost to IPv6 and get connection refused). + host = "127.0.0.1"; + } + + const portVal = options.config.src.emulators?.[emulator]?.port; + let port: number; + let portFixed: boolean; + if (portVal) { + port = parseInt(`${portVal}`, 10); + portFixed = true; + } else { + port = Constants.getDefaultPort(emulator); + portFixed = !FIND_AVAILBLE_PORT_BY_DEFAULT[emulator]; + } + return { + host, + port, + portFixed, + }; } /** @@ -714,12 +963,12 @@ export async function startAll(options: Options, showUI: boolean = true): Promis * @param exportPath * @param options */ -export async function exportEmulatorData(exportPath: string, options: any) { +export async function exportEmulatorData(exportPath: string, options: any, initiatedBy: string) { const projectId = options.project; if (!projectId) { throw new FirebaseError( "Could not determine project ID, make sure you're running in a Firebase project directory or add the --project flag.", - { exit: 1 } + { exit: 1 }, ); } @@ -727,23 +976,22 @@ export async function exportEmulatorData(exportPath: string, options: any) { if (!hubClient.foundHub()) { throw new FirebaseError( `Did not find any running emulators for project ${clc.bold(projectId)}.`, - { exit: 1 } + { exit: 1 }, ); } + let origin; try { - await hubClient.getStatus(); - } catch (e) { + origin = await hubClient.getStatus(); + } catch (e: any) { const filePath = EmulatorHub.getLocatorFilePath(projectId); throw new FirebaseError( `The emulator hub for ${projectId} did not respond to a status check. If this error continues try shutting down all running emulators and deleting the file ${filePath}`, - { exit: 1 } + { exit: 1 }, ); } - utils.logBullet( - `Found running emulator hub for project ${clc.bold(projectId)} at ${hubClient.origin}` - ); + utils.logBullet(`Found running emulator hub for project ${clc.bold(projectId)} at ${origin}`); // If the export target directory does not exist, we should attempt to create it const exportAbsPath = path.resolve(exportPath); @@ -754,17 +1002,19 @@ export async function exportEmulatorData(exportPath: string, options: any) { // Check if there is already an export there and prompt the user about deleting it const existingMetadata = HubExport.readMetadata(exportAbsPath); - if (existingMetadata && !(options.force || options.exportOnExit)) { + const isExportDirEmpty = fs.readdirSync(exportAbsPath).length === 0; + if ((existingMetadata || !isExportDirEmpty) && !(options.force || options.exportOnExit)) { if (options.noninteractive) { throw new FirebaseError( "Export already exists in the target directory, re-run with --force to overwrite.", - { exit: 1 } + { exit: 1 }, ); } - const prompt = await promptOnce({ - type: "confirm", - message: `The directory ${exportAbsPath} already contains export data. Exporting again to the same directory will overwrite all data. Do you want to continue?`, + const prompt = await confirm({ + message: `The directory ${exportAbsPath} is not empty. Existing files in this directory will be overwritten. Do you want to continue?`, + nonInteractive: options.nonInteractive, + force: options.force, default: false, }); @@ -775,8 +1025,8 @@ export async function exportEmulatorData(exportPath: string, options: any) { utils.logBullet(`Exporting data to: ${exportAbsPath}`); try { - await hubClient.postExport(exportAbsPath); - } catch (e) { + await hubClient.postExport({ path: exportAbsPath, initiatedBy }); + } catch (e: any) { throw new FirebaseError("Export request failed, see emulator logs for more information.", { exit: 1, original: e, diff --git a/src/emulator/databaseEmulator.ts b/src/emulator/databaseEmulator.ts index 0c436da4495..0f6a65743be 100644 --- a/src/emulator/databaseEmulator.ts +++ b/src/emulator/databaseEmulator.ts @@ -1,17 +1,17 @@ import * as chokidar from "chokidar"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import * as path from "path"; import * as http from "http"; -import * as api from "../api"; import * as downloadableEmulators from "./downloadableEmulators"; import { EmulatorInfo, EmulatorInstance, Emulators } from "../emulator/types"; import { Constants } from "./constants"; import { EmulatorRegistry } from "./registry"; import { EmulatorLogger } from "./emulatorLogger"; import { FirebaseError } from "../error"; -import * as parseBoltRules from "../parseBoltRules"; +import { parseBoltRules } from "../parseBoltRules"; +import { connectableHostname } from "../utils"; export interface DatabaseEmulatorArgs { port?: number; @@ -21,6 +21,7 @@ export interface DatabaseEmulatorArgs { functions_emulator_port?: number; functions_emulator_host?: string; auto_download?: boolean; + single_project_mode?: string; } export class DatabaseEmulator implements EmulatorInstance { @@ -43,13 +44,13 @@ export class DatabaseEmulator implements EmulatorInstance { this.logger.logLabeled( "WARN_ONCE", "database", - "Could not determine your Realtime Database instance name, so rules hot reloading is disabled." + "Could not determine your Realtime Database instance name, so rules hot reloading is disabled.", ); continue; } this.rulesWatcher = chokidar.watch(c.rules, { persistent: true, ignoreInitial: true }); - this.rulesWatcher.on("change", async (event, stats) => { + this.rulesWatcher.on("change", async () => { // There have been some race conditions reported (on Windows) where reading the // file too quickly after the watcher fires results in an empty file being read. // Adding a small delay prevents that at very little cost. @@ -58,13 +59,13 @@ export class DatabaseEmulator implements EmulatorInstance { this.logger.logLabeled( "BULLET", "database", - `Change detected, updating rules for ${c.instance}...` + `Change detected, updating rules for ${c.instance}...`, ); try { await this.updateRules(c.instance, c.rules); this.logger.logLabeled("SUCCESS", "database", "Rules updated."); - } catch (e) { + } catch (e: any) { this.logger.logLabeled("WARN", "database", this.prettyPrintRulesError(c.rules, e)); this.logger.logLabeled("WARN", "database", "Failed to update rules"); } @@ -83,8 +84,16 @@ export class DatabaseEmulator implements EmulatorInstance { if (!c.instance) { continue; } - - await this.updateRules(c.instance, c.rules); + try { + await this.updateRules(c.instance, c.rules); + } catch (e: any) { + const rulesError = this.prettyPrintRulesError(c.rules, e); + this.logger.logLabeled("WARN", "database", rulesError); + this.logger.logLabeled("WARN", "database", "Failed to update rules"); + throw new FirebaseError( + `Failed to load initial ${Constants.description(this.getName())} rules:\n${rulesError}`, + ); + } } } } @@ -94,7 +103,7 @@ export class DatabaseEmulator implements EmulatorInstance { } getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.DATABASE); + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.DATABASE); return { @@ -119,11 +128,11 @@ export class DatabaseEmulator implements EmulatorInstance { const readStream = fs.createReadStream(fPath); const { host, port } = this.getInfo(); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const req = http.request( { method: "PUT", - host, + host: connectableHostname(host), port, path: `/.json?ns=${ns}&disableTriggers=true&writeSizeLimit=unlimited`, headers: { @@ -143,7 +152,7 @@ export class DatabaseEmulator implements EmulatorInstance { }) .on("end", reject); } - } + }, ); req.on("error", reject); @@ -160,25 +169,49 @@ export class DatabaseEmulator implements EmulatorInstance { ? parseBoltRules(rulesPath).toString() : fs.readFileSync(rulesPath, "utf8"); - const info = this.getInfo(); try { - await api.request("PUT", `/.settings/rules.json?ns=${instance}`, { - origin: `http://${EmulatorRegistry.getInfoHostString(info)}`, + await EmulatorRegistry.client(Emulators.DATABASE).put(`/.settings/rules.json`, content, { headers: { Authorization: "Bearer owner" }, - data: content, - json: false, + queryParams: { ns: instance }, }); - } catch (e) { + } catch (e: any) { // The body is already parsed as JSON if (e.context && e.context.body) { throw e.context.body.error; } - throw e.original; + throw e.original ?? e; } } - private prettyPrintRulesError(filePath: string, error: string): string { + // TODO: tests + private prettyPrintRulesError(filePath: string, error: unknown): string { + let errStr; + switch (typeof error) { + case "string": + errStr = error; + break; + case "object": + if (error != null && "message" in error) { + const message = (error as { message: unknown }).message; + errStr = `${message}`; + if (typeof message === "string") { + try { + // message may be JSON with {error: string} in it + const parsed = JSON.parse(message); + if (typeof parsed === "object" && parsed.error) { + errStr = `${parsed.error}`; + } + } catch (_) { + // Probably not JSON, just output the string itself as above. + } + } + break; + } + // fallthrough + default: + errStr = `Unknown error: ${JSON.stringify(error)}`; + } const relativePath = path.relative(process.cwd(), filePath); - return `${clc.cyan(relativePath)}:${error.trim()}`; + return `${clc.cyan(relativePath)}:${errStr.trim()}`; } } diff --git a/src/emulator/dataconnectEmulator.ts b/src/emulator/dataconnectEmulator.ts new file mode 100644 index 00000000000..843d2fff7de --- /dev/null +++ b/src/emulator/dataconnectEmulator.ts @@ -0,0 +1,342 @@ +import * as childProcess from "child_process"; +import * as clc from "colorette"; + +import { dataConnectLocalConnString } from "../api"; +import { Constants } from "./constants"; +import { getPID, start, stop, downloadIfNecessary } from "./downloadableEmulators"; +import { EmulatorInfo, EmulatorInstance, Emulators, ListenSpec } from "./types"; +import { FirebaseError } from "../error"; +import { EmulatorLogger } from "./emulatorLogger"; +import { RC } from "../rc"; +import { BuildResult, requiresVector } from "../dataconnect/types"; +import { listenSpecsToString } from "./portUtils"; +import { Client, ClientResponse } from "../apiv2"; +import { EmulatorRegistry } from "./registry"; +import { logger } from "../logger"; +import { load } from "../dataconnect/load"; +import { isVSCodeExtension } from "../utils"; +import { EventEmitter } from "events"; + +export interface DataConnectEmulatorArgs { + projectId: string; + listen: ListenSpec[]; + configDir: string; + auto_download?: boolean; + rc: RC; +} + +export interface DataConnectGenerateArgs { + configDir: string; + connectorId: string; +} + +export interface DataConnectBuildArgs { + configDir: string; +} + +// TODO: More concrete typing for events. Can we use string unions? +export const dataConnectEmulatorEvents = new EventEmitter(); + +export class DataConnectEmulator implements EmulatorInstance { + private emulatorClient: DataConnectEmulatorClient; + private usingExistingEmulator: boolean = false; + + constructor(private args: DataConnectEmulatorArgs) { + this.emulatorClient = new DataConnectEmulatorClient(); + } + private logger = EmulatorLogger.forEmulator(Emulators.DATACONNECT); + + async start(): Promise { + try { + const info = await DataConnectEmulator.build({ configDir: this.args.configDir }); + if (requiresVector(info.metadata)) { + if (Constants.isDemoProject(this.args.projectId)) { + this.logger.logLabeled( + "WARN", + "Data Connect", + "Detected a 'demo-' project, but vector embeddings require a real project. Operations that use vector_embed will fail.", + ); + } else { + this.logger.logLabeled( + "WARN", + "Data Connect", + "Operations that use vector_embed will make calls to production Vertex AI", + ); + } + } + } catch (err: any) { + this.logger.log("DEBUG", `'fdc build' failed with error: ${err.message}`); + } + const alreadyRunning = await this.discoverRunningInstance(); + if (alreadyRunning) { + this.logger.logLabeled( + "INFO", + "Data Connect", + "Detected an instance of the emulator already running with your service, reusing it. This emulator will not be shut down at the end of this command.", + ); + this.usingExistingEmulator = true; + this.watchUnmanagedInstance(); + } else { + await start(Emulators.DATACONNECT, { + auto_download: this.args.auto_download, + listen: listenSpecsToString(this.args.listen), + config_dir: this.args.configDir, + }); + this.usingExistingEmulator = false; + } + if (!isVSCodeExtension()) { + await this.connectToPostgres(); + } + return; + } + + async connect(): Promise { + // TODO: Wait for 'Listening on address (HTTP + gRPC)' message to ensure that emulator binary is fully started. + const emuInfo = await this.emulatorClient.getInfo(); + if (!emuInfo) { + this.logger.logLabeled( + "ERROR", + "Data Connect", + "Could not connect to Data Connect emulator. Check dataconnect-debug.log for more details.", + ); + return Promise.reject(); + } + return Promise.resolve(); + } + + async stop(): Promise { + if (this.usingExistingEmulator) { + this.logger.logLabeled( + "INFO", + "Data Connect", + "Skipping cleanup of Data Connect emulator, as it was not started by this process.", + ); + return; + } + return stop(Emulators.DATACONNECT); + } + + getInfo(): EmulatorInfo { + return { + name: this.getName(), + listen: this.args.listen, + host: this.args.listen[0].address, + port: this.args.listen[0].port, + pid: getPID(Emulators.DATACONNECT), + timeout: 10_000, + }; + } + + getName(): Emulators { + return Emulators.DATACONNECT; + } + + static async generate(args: DataConnectGenerateArgs): Promise { + const commandInfo = await downloadIfNecessary(Emulators.DATACONNECT); + const cmd = [ + "--logtostderr", + "-v=2", + "generate", + `--config_dir=${args.configDir}`, + `--connector_id=${args.connectorId}`, + ]; + const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8" }); + + logger.info(res.stderr); + if (res.error) { + throw new FirebaseError(`Error starting up Data Connect generate: ${res.error.message}`, { + original: res.error, + }); + } + if (res.status !== 0) { + throw new FirebaseError( + `Unable to generate your Data Connect SDKs (exit code ${res.status}): ${res.stderr}`, + ); + } + return res.stdout; + } + + static async build(args: DataConnectBuildArgs): Promise { + const commandInfo = await downloadIfNecessary(Emulators.DATACONNECT); + const cmd = ["--logtostderr", "-v=2", "build", `--config_dir=${args.configDir}`]; + + const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8" }); + if (res.error) { + throw new FirebaseError(`Error starting up Data Connect build: ${res.error.message}`, { + original: res.error, + }); + } + if (res.status !== 0) { + throw new FirebaseError( + `Unable to build your Data Connect schema and connectors (exit code ${res.status}): ${res.stderr}`, + ); + } + + if (res.stderr) { + EmulatorLogger.forEmulator(Emulators.DATACONNECT).log("DEBUG", res.stderr); + } + + try { + return JSON.parse(res.stdout) as BuildResult; + } catch (err) { + // JSON parse errors are unreadable. + throw new FirebaseError(`Unable to parse 'fdc build' output: ${res.stdout ?? res.stderr}`); + } + } + + private getLocalConectionString() { + if (dataConnectLocalConnString()) { + return dataConnectLocalConnString(); + } + return this.args.rc.getDataconnect()?.postgres?.localConnectionString; + } + + private async discoverRunningInstance(): Promise { + const emuInfo = await this.emulatorClient.getInfo(); + if (!emuInfo) { + return false; + } + const serviceInfo = await load(this.args.projectId, this.args.configDir); + const sameService = emuInfo.services.find( + (s) => serviceInfo.dataConnectYaml.serviceId === s.serviceId, + ); + if (!sameService) { + throw new FirebaseError( + `There is a Data Connect emulator already running on ${this.args.listen[0].address}:${this.args.listen[0].port}, but it is emulating a different service. Please stop that instance of the Data Connect emulator, or specify a different port in 'firebase.json'`, + ); + } + if ( + sameService.connectionString && + sameService.connectionString !== this.getLocalConectionString() + ) { + throw new FirebaseError( + `There is a Data Connect emulator already running, but it is using a different Postgres connection string. Please stop that instance of the Data Connect emulator, or specify a different port in 'firebase.json'`, + ); + } + return true; + } + + private watchUnmanagedInstance() { + return setInterval(async () => { + if (!this.usingExistingEmulator) { + return; + } + const emuInfo = await this.emulatorClient.getInfo(); + if (!emuInfo) { + this.logger.logLabeled( + "INFO", + "Data Connect", + "The already running emulator seems to have shut down. Starting a new instance of the Data Connect emulator...", + ); + // If the other emulator was shut down, we spin our own copy up + // TODO: Guard against multiple simultaneous calls here. + await this.start(); + dataConnectEmulatorEvents.emit("restart"); + } + }, 5000); // Check uptime every 5 seconds + } + + public async connectToPostgres( + localConnectionString?: string, + database?: string, + serviceId?: string, + ): Promise { + const connectionString = localConnectionString ?? this.getLocalConectionString(); + if (!connectionString) { + const msg = `No Postgres connection string found in '.firebaserc'. The Data Connect emulator will not be able to execute operations. +Run ${clc.bold("firebase setup:emulators:dataconnect")} to set up a Postgres connection.`; + throw new FirebaseError(msg); + } + // The Data Connect emulator does not immediately start listening after started + // so we retry this call with a brief backoff. + const MAX_RETRIES = 3; + for (let i = 1; i <= MAX_RETRIES; i++) { + try { + this.logger.logLabeled("DEBUG", "Data Connect", `Connecting to ${connectionString}}`); + await this.emulatorClient.configureEmulator({ connectionString, database, serviceId }); + return true; + } catch (err: any) { + if (i === MAX_RETRIES) { + throw err; + } + this.logger.logLabeled( + "DEBUG", + "Data Connect", + `Retrying connectToPostgress call (${i} of ${MAX_RETRIES} attempts): ${err}`, + ); + await new Promise((resolve) => setTimeout(resolve, 800)); + } + } + return false; + } +} + +type ConfigureEmulatorRequest = { + // Defaults to the local service in dataconnect.yaml if not provided + serviceId?: string; + // The Postgres connection string to connect the new service to. This is + // required in order to configure the emulator service. + connectionString: string; + // The Postgres database to connect the new service to. If this field is + // populated, then any database specified in the connection_string will be + // overwritten. + database?: string; +}; + +type GetInfoResponse = { + // Version number of the emulator. + version: string; + // List of services currently running on the emulator. + services: { + // ID of this service. + serviceId: string; + // The Postgres connection string that this service uses. + connectionString: string; + }[]; +}; + +export class DataConnectEmulatorClient { + private client: Client | undefined = undefined; + + public async configureEmulator(body: ConfigureEmulatorRequest): Promise> { + if (!this.client) { + this.client = EmulatorRegistry.client(Emulators.DATACONNECT); + } + try { + const res = await this.client.post( + "emulator/configure", + body, + ); + return res; + } catch (err: any) { + if (err.status === 500) { + throw new FirebaseError(`Data Connect emulator: ${err?.context?.body?.message}`); + } + throw err; + } + } + + public async getInfo(): Promise { + if (!this.client) { + this.client = EmulatorRegistry.client(Emulators.DATACONNECT); + } + return getInfo(this.client); + } +} + +export async function checkIfDataConnectEmulatorRunningOnAddress(l: ListenSpec) { + const client = new Client({ + urlPrefix: `http:/${l.family === "IPv6" ? `[${l.address}]` : l.address}:${l.port}`, + auth: false, + }); + return getInfo(client); +} + +async function getInfo(client: Client): Promise { + try { + const res = await client.get("emulator/info"); + return res.body; + } catch (err) { + return; + } +} diff --git a/src/emulator/dns.spec.ts b/src/emulator/dns.spec.ts new file mode 100644 index 00000000000..763d52d5c2a --- /dev/null +++ b/src/emulator/dns.spec.ts @@ -0,0 +1,115 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { IPV4_LOOPBACK, IPV6_LOOPBACK, Resolver } from "./dns"; + +const IPV4_ADDR1 = { address: "169.254.20.1", family: 4 }; +const IPV4_ADDR2 = { address: "169.254.20.2", family: 4 }; +const IPV6_ADDR1 = { address: "fe80::1", family: 6 }; +const IPV6_ADDR2 = { address: "fe80::2", family: 6 }; + +describe("Resolver", () => { + describe("#lookupFirst", () => { + it("should return the first value of result", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("example.test")).to.eventually.eql(IPV4_ADDR1); + }); + + it("should prefer IPv4 addresss using the underlying lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("example.test")).to.eventually.eql(IPV4_ADDR1); + expect(lookup).to.be.calledOnceWithExactly("example.test", sinon.match({ verbatim: false })); + }); + + it("should return cached result if available", async () => { + const lookup = sinon.fake((hostname: string) => { + return hostname === "example1.test" ? [IPV4_ADDR1, IPV6_ADDR1] : [IPV4_ADDR2, IPV6_ADDR2]; + }); + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("example1.test")).to.eventually.eql(IPV4_ADDR1); + await expect(resolver.lookupFirst("example1.test")).to.eventually.eql(IPV4_ADDR1); + expect(lookup).to.be.calledOnce; // the second call should not trigger lookup + + lookup.resetHistory(); + // A call with a different name should cause a cache miss. + await expect(resolver.lookupFirst("example2.test")).to.eventually.eql(IPV4_ADDR2); + expect(lookup).to.be.calledOnce; + }); + + it("should pre-populate localhost in cache to resolve to IPv4 loopback address", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("localhost")).to.eventually.eql(IPV4_LOOPBACK); + expect(lookup).not.to.be.called; + }); + + it("should parse and return IPv4 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("127.0.0.1")).to.eventually.eql(IPV4_LOOPBACK); + expect(lookup).not.to.be.called; + }); + + it("should parse and return IPv6 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupFirst("::1")).to.eventually.eql(IPV6_LOOPBACK); + expect(lookup).not.to.be.called; + }); + }); + + describe("#lookupAll", () => { + it("should return all addresses returned", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("example.test")).to.eventually.eql([IPV4_ADDR1, IPV4_ADDR2]); + }); + + it("should request IPv4 addresses to be listed first using the underlying lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV4_ADDR2]); + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("example.test")).to.eventually.eql([IPV4_ADDR1, IPV4_ADDR2]); + expect(lookup).to.be.calledOnceWithExactly("example.test", sinon.match({ verbatim: false })); + }); + + it("should return cached results if available", async () => { + const lookup = sinon.fake((hostname: string) => { + return hostname === "example1.test" ? [IPV4_ADDR1, IPV6_ADDR1] : [IPV4_ADDR2, IPV6_ADDR2]; + }); + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("example1.test")).to.eventually.eql([IPV4_ADDR1, IPV6_ADDR1]); + await expect(resolver.lookupAll("example1.test")).to.eventually.eql([IPV4_ADDR1, IPV6_ADDR1]); + expect(lookup).to.be.calledOnce; // the second call should not trigger lookup + + lookup.resetHistory(); + // A call with a different name should cause a cache miss. + await expect(resolver.lookupAll("example2.test")).to.eventually.eql([IPV4_ADDR2, IPV6_ADDR2]); + expect(lookup).to.be.calledOnce; + }); + + it("should pre-populate localhost in cache to resolve to IPv4 + IPv6 loopback addresses (in that order)", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("localhost")).to.eventually.eql([ + IPV4_LOOPBACK, + IPV6_LOOPBACK, + ]); + expect(lookup).not.to.be.called; + }); + }); + + it("should parse and return IPv4 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("127.0.0.1")).to.eventually.eql([IPV4_LOOPBACK]); + expect(lookup).not.to.be.called; + }); + + it("should parse and return IPv6 addresses without lookup", async () => { + const lookup = sinon.fake.resolves([IPV4_ADDR1, IPV6_ADDR1]); // ignored + const resolver = new Resolver(lookup); + await expect(resolver.lookupAll("::1")).to.eventually.eql([IPV6_LOOPBACK]); + expect(lookup).not.to.be.called; + }); +}); diff --git a/src/emulator/dns.ts b/src/emulator/dns.ts new file mode 100644 index 00000000000..1226f5e89df --- /dev/null +++ b/src/emulator/dns.ts @@ -0,0 +1,101 @@ +import { LookupAddress, LookupAllOptions, promises as dnsPromises } from "node:dns"; // Not using "dns/promises" for Node 14 compatibility. +import { isIP } from "node:net"; +import { logger } from "../logger"; + +export const IPV4_LOOPBACK = { address: "127.0.0.1", family: 4 } as const; +export const IPV6_LOOPBACK = { address: "::1", family: 6 } as const; +export const IPV4_UNSPECIFIED = { address: "0.0.0.0", family: 4 } as const; +export const IPV6_UNSPECIFIED = { address: "::", family: 6 } as const; + +/** + * Resolves hostnames to IP addresses consistently. + * + * The result(s) for a single hostname is cached in memory to ensure consistency + * throughout the lifetime or a single process (i.e. CLI command invocation). + */ +export class Resolver { + /** + * The default resolver. Preferred in all normal CLI operations. + */ + public static DEFAULT = new Resolver(); + + private cache = new Map([ + // Pre-populate cache with localhost (the most common hostname used in + // emulators) for quicker startup and better consistency across OSes. + ["localhost", [IPV4_LOOPBACK, IPV6_LOOPBACK]], + ]); + + /** + * Create a new Resolver instance with its own dedicated cache. + * + * @param lookup an underlying DNS lookup function (useful in tests) + */ + public constructor( + private lookup: ( + hostname: string, + options: LookupAllOptions, + ) => Promise = dnsPromises.lookup, + ) {} + + /** + * Returns the first IP address that a hostname map to, ignoring others. + * + * If possible, prefer `lookupAll` and handle all results instead, since the + * first one may not be what the user wants. Especially, when a domain name is + * specified as the listening hostname of a server, listening on both IPv4 and + * IPv6 addresses may be closer to user intention. + * + * A successful lookup will add the results to the cache, which will be used + * to serve subsequent requests to the same hostname on the same `Resolver`. + * + * @param hostname the hostname to resolve + * @return the first IP address (perferrably IPv4 for compatibility) + */ + async lookupFirst(hostname: string): Promise { + const addresses = await this.lookupAll(hostname); + if (addresses.length === 1) { + return addresses[0]; + } + + // Log a debug message when discarding additional results: + const result = addresses[0]; + const discarded: string[] = []; + for (let i = 1; i < addresses.length; i++) { + discarded.push(result.address); + } + logger.debug( + `Resolved hostname "${hostname}" to the first result "${ + result.address + }" (ignoring candidates: ${discarded.join(",")}).`, + ); + return result; + } + + /** + * Returns all IP addresses that a hostname map to, IPv4 first (if present). + * + * A successful lookup will add the results to the cache, which will be used + * to serve subsequent requests to the same hostname on the same `Resolver`. + * + * @param hostname the hostname to resolve + * @return IP addresses (IPv4 addresses before IPv6 ones for compatibility) + */ + async lookupAll(hostname: string): Promise { + const family = isIP(hostname); + if (family > 0) { + return [{ family, address: hostname }]; + } + // We may want to make this case-insensitive if customers run into issues. + const cached = this.cache.get(hostname); + if (cached) { + return cached; + } + const addresses = await this.lookup(hostname, { + // Return IPv4 addresses first (for backwards compatibility). + verbatim: false, + all: true, + }); + this.cache.set(hostname, addresses); + return addresses; + } +} diff --git a/src/emulator/download.ts b/src/emulator/download.ts index 0bc3e0a1db6..76bd0a26008 100644 --- a/src/emulator/download.ts +++ b/src/emulator/download.ts @@ -2,11 +2,11 @@ import * as crypto from "crypto"; import * as fs from "fs-extra"; import * as path from "path"; import * as tmp from "tmp"; -import * as unzipper from "unzipper"; import { EmulatorLogger } from "./emulatorLogger"; import { EmulatorDownloadDetails, DownloadableEmulators } from "./types"; import { FirebaseError } from "../error"; +import { unzip } from "../unzip"; import * as downloadableEmulators from "./downloadableEmulators"; import * as downloadUtils from "../downloadUtils"; @@ -14,14 +14,22 @@ tmp.setGracefulCleanup(); export async function downloadEmulator(name: DownloadableEmulators): Promise { const emulator = downloadableEmulators.getDownloadDetails(name); + if (emulator.localOnly) { + EmulatorLogger.forEmulator(name).logLabeled( + "WARN", + name, + `Env variable override detected, skipping download. Using ${emulator} emulator at ${emulator.binaryPath}`, + ); + return; + } EmulatorLogger.forEmulator(name).logLabeled( "BULLET", name, - `downloading ${path.basename(emulator.downloadPath)}...` + `downloading ${path.basename(emulator.downloadPath)}...`, ); fs.ensureDirSync(emulator.opts.cacheDir); - const tmpfile = await downloadUtils.downloadToTmp(emulator.opts.remoteUrl); + const tmpfile = await downloadUtils.downloadToTmp(emulator.opts.remoteUrl, !!emulator.opts.auth); if (!emulator.opts.skipChecksumAndSize) { await validateSize(tmpfile, emulator.opts.expectedSize); @@ -43,19 +51,41 @@ export async function downloadEmulator(name: DownloadableEmulators): Promise { - return new Promise((resolve, reject) => { - fs.createReadStream(zipPath) - .pipe(unzipper.Extract({ path: unzipDir })) // eslint-disable-line new-cap - .on("error", reject) - .on("finish", resolve); - }); +export async function downloadExtensionVersion( + extensionVersionRef: string, + sourceDownloadUri: string, + targetDir: string, +): Promise { + const emulatorLogger = EmulatorLogger.forExtension({ ref: extensionVersionRef }); + emulatorLogger.logLabeled( + "BULLET", + "extensions", + `Starting download for ${extensionVersionRef} source code to ${targetDir}..`, + ); + try { + fs.mkdirSync(targetDir); + } catch (err) { + emulatorLogger.logLabeled( + "BULLET", + "extensions", + `cache directory for ${extensionVersionRef} already exists...`, + ); + } + emulatorLogger.logLabeled("BULLET", "extensions", `downloading ${sourceDownloadUri}...`); + const sourceCodeZip = await downloadUtils.downloadToTmp(sourceDownloadUri); + await unzip(sourceCodeZip, targetDir); + fs.chmodSync(targetDir, 0o755); + + emulatorLogger.logLabeled("BULLET", "extensions", `Downloaded to ${targetDir}...`); + // TODO: We should not need to do this wait + // However, when I remove this, unzipDir doesn't contain everything yet. + await new Promise((resolve) => setTimeout(resolve, 1000)); } function removeOldFiles( name: DownloadableEmulators, emulator: EmulatorDownloadDetails, - removeAllVersions = false + removeAllVersions = false, ): void { const currentLocalPath = emulator.downloadPath; const currentUnzipPath = emulator.unzipDir; @@ -77,7 +107,7 @@ function removeOldFiles( EmulatorLogger.forEmulator(name).logLabeled( "BULLET", name, - `Removing outdated emulator files: ${file}` + `Removing outdated emulator files: ${file}`, ); fs.removeSync(fullFilePath); } @@ -95,8 +125,8 @@ function validateSize(filepath: string, expectedSize: number): Promise { : reject( new FirebaseError( `download failed, expected ${expectedSize} bytes but got ${stat.size}`, - { exit: 1 } - ) + { exit: 1 }, + ), ); }); } @@ -116,8 +146,8 @@ function validateChecksum(filepath: string, expectedChecksum: string): Promise { + const tempEnvVars: Record = { + firestore: "", + database: "", + pubsub: "", + }; + let chmodStub: sinon.SinonStub; + beforeEach(() => { + chmodStub = sinon.stub(fs, "chmodSync").returns(); + tempEnvVars["firestore"] = process.env["FIRESTORE_EMULATOR_BINARY_PATH"] ?? ""; + tempEnvVars["database"] = process.env["DATABASE_EMULATOR_BINARY_PATH"] ?? ""; + tempEnvVars["pubsub"] = process.env["PUBSUB_EMULATOR_BINARY_PATH"] ?? ""; + delete process.env["FIRESTORE_EMULATOR_BINARY_PATH"]; + delete process.env["DATABASE_EMULATOR_BINARY_PATH"]; + delete process.env["PUBSUB_EMULATOR_BINARY_PATH"]; + }); + + afterEach(() => { + chmodStub.restore(); + process.env["FIRESTORE_EMULATOR_BINARY_PATH"] = tempEnvVars["firestore"]; + process.env["DATABASE_EMULATOR_BINARY_PATH"] = tempEnvVars["database"]; + process.env["PUBSUB_EMULATOR_BINARY_PATH"] = tempEnvVars["pubsub"]; + }); + it("should match the basename of remoteUrl", () => { + checkDownloadPath(Emulators.FIRESTORE); + checkDownloadPath(Emulators.DATABASE); + checkDownloadPath(Emulators.PUBSUB); + }); + + it("should apply environment varable overrides", () => { + process.env["FIRESTORE_EMULATOR_BINARY_PATH"] = "my/fake/firestore"; + process.env["DATABASE_EMULATOR_BINARY_PATH"] = "my/fake/database"; + process.env["PUBSUB_EMULATOR_BINARY_PATH"] = "my/fake/pubsub"; + + expect(downloadableEmulators.getDownloadDetails(Emulators.FIRESTORE).binaryPath).to.equal( + "my/fake/firestore", + ); + expect(downloadableEmulators.getDownloadDetails(Emulators.DATABASE).binaryPath).to.equal( + "my/fake/database", + ); + expect(downloadableEmulators.getDownloadDetails(Emulators.PUBSUB).binaryPath).to.equal( + "my/fake/pubsub", + ); + expect(chmodStub.callCount).to.equal(3); + }); +}); diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts old mode 100644 new mode 100755 index 08fc674b3ff..a3a4a6019af --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -4,6 +4,7 @@ import { DownloadableEmulatorCommand, DownloadableEmulatorDetails, EmulatorDownloadDetails, + EmulatorUpdateDetails, } from "./types"; import { Constants } from "./constants"; @@ -12,87 +13,176 @@ import * as childProcess from "child_process"; import * as utils from "../utils"; import { EmulatorLogger } from "./emulatorLogger"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs-extra"; import * as path from "path"; import * as os from "os"; import { EmulatorRegistry } from "./registry"; import { downloadEmulator } from "../emulator/download"; -import { previews } from "../previews"; +import * as experiments from "../experiments"; const EMULATOR_INSTANCE_KILL_TIMEOUT = 4000; /* ms */ const CACHE_DIR = process.env.FIREBASE_EMULATORS_PATH || path.join(os.homedir(), ".cache", "firebase", "emulators"); +const EMULATOR_UPDATE_DETAILS: { [s in DownloadableEmulators]: EmulatorUpdateDetails } = { + database: { + version: "4.11.2", + expectedSize: 34495935, + expectedChecksum: "2fd771101c0e1f7898c04c9204f2ce63", + }, + firestore: { + version: "1.19.7", + expectedSize: 66438992, + expectedChecksum: "aec233bea95c5cfab03881574ec16d6c", + }, + storage: { + version: "1.1.3", + expectedSize: 52892936, + expectedChecksum: "2ca11ec1193003bea89f806cc085fa25", + }, + ui: experiments.isEnabled("emulatoruisnapshot") + ? { version: "SNAPSHOT", expectedSize: -1, expectedChecksum: "" } + : { + version: "1.12.1", + expectedSize: 3498269, + expectedChecksum: "a7f4398a00e5ca22abdcd78dc3877d00", + }, + pubsub: { + version: "0.8.14", + expectedSize: 66786933, + expectedChecksum: "a9025b3e53fdeafd2969ccb3ba1e1d38", + }, + dataconnect: + process.platform === "darwin" + ? { + version: "1.2.2", + expectedSize: 24007488, + expectedChecksum: "c1fb77895203681479ee5dd22d57249f", + } + : process.platform === "win32" + ? { + version: "1.2.2", + expectedSize: 24414208, + expectedChecksum: "7e263c2b2bc9055ead2db8102e883534", + } + : { + version: "1.2.2", + expectedSize: 24023300, + expectedChecksum: "12467418226ac9657fb64b4d719d0e1d", + }, +}; + export const DownloadDetails: { [s in DownloadableEmulators]: EmulatorDownloadDetails } = { database: { - downloadPath: path.join(CACHE_DIR, "firebase-database-emulator-v4.7.2.jar"), - version: "4.7.2", + downloadPath: path.join( + CACHE_DIR, + `firebase-database-emulator-v${EMULATOR_UPDATE_DETAILS.database.version}.jar`, + ), + version: EMULATOR_UPDATE_DETAILS.database.version, opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.7.2.jar", - expectedSize: 28910604, - expectedChecksum: "264e5df0c0661c064ef7dc9ce8179aba", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v${EMULATOR_UPDATE_DETAILS.database.version}.jar`, + expectedSize: EMULATOR_UPDATE_DETAILS.database.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.database.expectedChecksum, namePrefix: "firebase-database-emulator", }, }, firestore: { - downloadPath: path.join(CACHE_DIR, "cloud-firestore-emulator-v1.13.1.jar"), - version: "1.13.1", + downloadPath: path.join( + CACHE_DIR, + `cloud-firestore-emulator-v${EMULATOR_UPDATE_DETAILS.firestore.version}.jar`, + ), + version: EMULATOR_UPDATE_DETAILS.firestore.version, opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.13.1.jar", - expectedSize: 60486708, - expectedChecksum: "e0590880408eacb790874643147c0081", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v${EMULATOR_UPDATE_DETAILS.firestore.version}.jar`, + expectedSize: EMULATOR_UPDATE_DETAILS.firestore.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.firestore.expectedChecksum, namePrefix: "cloud-firestore-emulator", }, }, storage: { - downloadPath: path.join(CACHE_DIR, "cloud-storage-rules-runtime-v1.0.1.jar"), - version: "1.0.1", + downloadPath: path.join( + CACHE_DIR, + `cloud-storage-rules-runtime-v${EMULATOR_UPDATE_DETAILS.storage.version}.jar`, + ), + version: EMULATOR_UPDATE_DETAILS.storage.version, opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-storage-rules-runtime-v1.0.1.jar", - expectedSize: 32729999, - expectedChecksum: "1a441f5e16c17aa7a27db71c9c9186d5", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-storage-rules-runtime-v${EMULATOR_UPDATE_DETAILS.storage.version}.jar`, + expectedSize: EMULATOR_UPDATE_DETAILS.storage.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.storage.expectedChecksum, namePrefix: "cloud-storage-rules-emulator", }, }, ui: { - version: "1.6.4", - downloadPath: path.join(CACHE_DIR, "ui-v1.6.4.zip"), - unzipDir: path.join(CACHE_DIR, "ui-v1.6.4"), - binaryPath: path.join(CACHE_DIR, "ui-v1.6.4", "server.bundle.js"), + version: EMULATOR_UPDATE_DETAILS.ui.version, + downloadPath: path.join(CACHE_DIR, `ui-v${EMULATOR_UPDATE_DETAILS.ui.version}.zip`), + unzipDir: path.join(CACHE_DIR, `ui-v${EMULATOR_UPDATE_DETAILS.ui.version}`), + binaryPath: path.join( + CACHE_DIR, + `ui-v${EMULATOR_UPDATE_DETAILS.ui.version}`, + "server", + "server.mjs", + ), opts: { cacheDir: CACHE_DIR, - remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.6.4.zip", - expectedSize: 3757300, - expectedChecksum: "20d4ee71e4ff7527b1843b6a8636142e", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v${EMULATOR_UPDATE_DETAILS.ui.version}.zip`, + expectedSize: EMULATOR_UPDATE_DETAILS.ui.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.ui.expectedChecksum, + skipCache: experiments.isEnabled("emulatoruisnapshot"), + skipChecksumAndSize: experiments.isEnabled("emulatoruisnapshot"), namePrefix: "ui", }, }, pubsub: { - downloadPath: path.join(CACHE_DIR, "pubsub-emulator-0.1.0.zip"), - version: "0.1.0", - unzipDir: path.join(CACHE_DIR, "pubsub-emulator-0.1.0"), + downloadPath: path.join( + CACHE_DIR, + `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}.zip`, + ), + version: EMULATOR_UPDATE_DETAILS.pubsub.version, + unzipDir: path.join(CACHE_DIR, `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}`), binaryPath: path.join( CACHE_DIR, - "pubsub-emulator-0.1.0", - `pubsub-emulator/bin/cloud-pubsub-emulator${process.platform === "win32" ? ".bat" : ""}` + `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}`, + `pubsub-emulator/bin/cloud-pubsub-emulator${process.platform === "win32" ? ".bat" : ""}`, ), opts: { cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.1.0.zip", - expectedSize: 36623622, - expectedChecksum: "81704b24737d4968734d3e175f4cde71", + remoteUrl: `https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}.zip`, + expectedSize: EMULATOR_UPDATE_DETAILS.pubsub.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.pubsub.expectedChecksum, namePrefix: "pubsub-emulator", }, }, + // TODO: Add Windows binary here as well + dataconnect: { + downloadPath: path.join( + CACHE_DIR, + `dataconnect-emulator-${EMULATOR_UPDATE_DETAILS.dataconnect.version}${process.platform === "win32" ? ".exe" : ""}`, + ), + version: EMULATOR_UPDATE_DETAILS.dataconnect.version, + binaryPath: path.join( + CACHE_DIR, + `dataconnect-emulator-${EMULATOR_UPDATE_DETAILS.dataconnect.version}${process.platform === "win32" ? ".exe" : ""}`, + ), + opts: { + cacheDir: CACHE_DIR, + remoteUrl: + process.platform === "darwin" + ? `https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-v${EMULATOR_UPDATE_DETAILS.dataconnect.version}` + : process.platform === "win32" + ? `https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-v${EMULATOR_UPDATE_DETAILS.dataconnect.version}` + : `https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-v${EMULATOR_UPDATE_DETAILS.dataconnect.version}`, + expectedSize: EMULATOR_UPDATE_DETAILS.dataconnect.expectedSize, + expectedChecksum: EMULATOR_UPDATE_DETAILS.dataconnect.expectedChecksum, + skipChecksumAndSize: false, + namePrefix: "dataconnect-emulator", + auth: false, + }, + }, }; const EmulatorDetails: { [s in DownloadableEmulators]: DownloadableEmulatorDetails } = { @@ -121,14 +211,26 @@ const EmulatorDetails: { [s in DownloadableEmulators]: DownloadableEmulatorDetai instance: null, stdout: null, }, + dataconnect: { + name: Emulators.DATACONNECT, + instance: null, + stdout: null, + }, }; const Commands: { [s in DownloadableEmulators]: DownloadableEmulatorCommand } = { database: { binary: "java", args: ["-Duser.language=en", "-jar", getExecPath(Emulators.DATABASE)], - optionalArgs: ["port", "host", "functions_emulator_port", "functions_emulator_host"], + optionalArgs: [ + "port", + "host", + "functions_emulator_port", + "functions_emulator_host", + "single_project_mode", + ], joinArgs: false, + shell: false, }, firestore: { binary: "java", @@ -143,37 +245,59 @@ const Commands: { [s in DownloadableEmulators]: DownloadableEmulatorCommand } = "webchannel_port", "host", "rules", + "websocket_port", "functions_emulator", "seed_from_export", + "project_id", + "single_project_mode", + // TODO(christhompson) Re-enable after firestore accepts this flag. + // "single_project_mode_error", ], joinArgs: false, + shell: false, }, storage: { // This is for the Storage Emulator rules runtime, which is started // separately in ./storage/runtime.ts (not via the start function below). binary: "java", args: [ - "-jar", // Required for rules error/warning messages, which are in English only. // Attempts to fetch the messages in another language leads to crashes. "-Duser.language=en", + "-jar", getExecPath(Emulators.STORAGE), "serve", ], optionalArgs: [], joinArgs: false, + shell: false, }, pubsub: { - binary: getExecPath(Emulators.PUBSUB)!, + binary: `${getExecPath(Emulators.PUBSUB)!}`, args: [], optionalArgs: ["port", "host"], joinArgs: true, + shell: true, }, ui: { binary: "node", args: [getExecPath(Emulators.UI)], optionalArgs: [], joinArgs: false, + shell: false, + }, + dataconnect: { + binary: `${getExecPath(Emulators.DATACONNECT)}`, + args: ["--logtostderr", "-v=2", "dev"], + optionalArgs: [ + "listen", + "config_dir", + "disable_sdk_generation", + "resolvers_emulator", + "rpc_retry_count", + ], + joinArgs: true, + shell: false, }, }; @@ -196,10 +320,9 @@ export function getLogFileName(name: string): string { */ export function _getCommand( emulator: DownloadableEmulators, - args: { [s: string]: any } + args: { [s: string]: any }, ): DownloadableEmulatorCommand { const baseCmd = Commands[emulator]; - const defaultPort = Constants.getDefaultPort(emulator); if (!args.port) { args.port = defaultPort; @@ -249,6 +372,7 @@ export function _getCommand( args: cmdLineArgs, optionalArgs: baseCmd.optionalArgs, joinArgs: baseCmd.joinArgs, + shell: baseCmd.shell, }; } @@ -261,7 +385,7 @@ async function _fatal(emulator: Emulators, errorMsg: string): Promise { logger.logLabeled( "WARN", emulator, - `Fatal error occurred: \n ${errorMsg}, \n stopping all running emulators` + `Fatal error occurred: \n ${errorMsg}, \n stopping all running emulators`, ); await EmulatorRegistry.stopAll(); } finally { @@ -269,28 +393,41 @@ async function _fatal(emulator: Emulators, errorMsg: string): Promise { } } +/** + * Handle errors in an emulator process. + */ export async function handleEmulatorProcessError(emulator: Emulators, err: any): Promise { const description = Constants.description(emulator); if (err.path === "java" && err.code === "ENOENT") { await _fatal( emulator, - `${description} has exited because java is not installed, you can install it from https://openjdk.java.net/install/` + `${description} has exited because java is not installed, you can install it from https://openjdk.java.net/install/`, ); } else { await _fatal(emulator, `${description} has exited: ${err}`); } } +/** + * Do the selected list of emulators depend on the JRE. + */ +export function requiresJava(emulator: Emulators): boolean { + if (emulator in Commands) { + return Commands[emulator as keyof typeof Commands].binary === "java"; + } + return false; +} + async function _runBinary( emulator: DownloadableEmulatorDetails, command: DownloadableEmulatorCommand, - extraEnv: NodeJS.ProcessEnv + extraEnv: Partial, ): Promise { return new Promise((resolve) => { const logger = EmulatorLogger.forEmulator(emulator.name); emulator.stdout = fs.createWriteStream(getLogFileName(emulator.name)); try { - emulator.instance = childProcess.spawn(command.binary, command.args, { + const opts: childProcess.SpawnOptions = { env: { ...process.env, ...extraEnv }, // `detached` must be true as else a SIGINT (Ctrl-c) will stop the child process before we can handle a // graceful shutdown and call `downloadableEmulators.stop(...)` ourselves. @@ -298,15 +435,22 @@ async function _runBinary( // related to this issue: https://github.com/grpc/grpc-java/pull/6512 detached: true, stdio: ["inherit", "pipe", "pipe"], - }); - } catch (e) { + }; + if (command.shell && utils.IS_WINDOWS) { + opts.shell = true; + if (command.binary.includes(" ")) { + command.binary = `"${command.binary}"`; + } + } + emulator.instance = childProcess.spawn(command.binary, command.args, opts); + } catch (e: any) { if (e.code === "EACCES") { // Known issue when WSL users don't have java // https://github.com/Microsoft/WSL/issues/3886 logger.logLabeled( "WARN", emulator.name, - `Could not spawn child process for emulator, check that java is installed and on your $PATH.` + `Could not spawn child process for emulator, check that java is installed and on your $PATH.`, ); } _fatal(emulator.name, e); @@ -322,14 +466,14 @@ async function _runBinary( logger.logLabeled( "BULLET", emulator.name, - `${description} logging to ${clc.bold(getLogFileName(emulator.name))}` + `${description} logging to ${clc.bold(getLogFileName(emulator.name))}`, ); - emulator.instance.stdout.on("data", (data) => { + emulator.instance.stdout?.on("data", (data) => { logger.log("DEBUG", data.toString()); emulator.stdout.write(data); }); - emulator.instance.stderr.on("data", (data) => { + emulator.instance.stderr?.on("data", (data) => { logger.log("DEBUG", data.toString()); emulator.stdout.write(data); @@ -337,7 +481,7 @@ async function _runBinary( logger.logLabeled( "WARN", emulator.name, - "Unsupported java version, make sure java --version reports 1.8 or higher." + "Unsupported java version, make sure java --version reports 1.8 or higher.", ); } }); @@ -361,7 +505,21 @@ async function _runBinary( * @param emulator */ export function getDownloadDetails(emulator: DownloadableEmulators): EmulatorDownloadDetails { - return DownloadDetails[emulator]; + const details = DownloadDetails[emulator]; + const pathOverride = process.env[`${emulator.toUpperCase()}_EMULATOR_BINARY_PATH`]; + if (pathOverride) { + const logger = EmulatorLogger.forEmulator(emulator); + logger.logLabeled( + "WARN", + emulator, + `Env variable override detected. Using ${emulator} emulator at ${pathOverride}`, + ); + details.downloadPath = pathOverride; + details.binaryPath = pathOverride; + details.localOnly = true; + fs.chmodSync(pathOverride, 0o755); + } + return details; } /** @@ -387,7 +545,9 @@ export async function stop(targetName: DownloadableEmulators): Promise { const emulator = get(targetName); return new Promise((resolve, reject) => { const logger = EmulatorLogger.forEmulator(emulator.name); - if (emulator.instance) { + + // kill(0) does not end the process, it just checks for existence. See https://man7.org/linux/man-pages/man2/kill.2.html#:~:text=If%20sig%20is%200%2C%20 + if (emulator.instance && emulator.instance.kill(0)) { const killTimeout = setTimeout(() => { const pid = emulator.instance ? emulator.instance.pid : -1; const errorMsg = @@ -395,7 +555,6 @@ export async function stop(targetName: DownloadableEmulators): Promise { logger.log("DEBUG", errorMsg); reject(new FirebaseError(emulator.name + ": " + errorMsg)); }, EMULATOR_INSTANCE_KILL_TIMEOUT); - emulator.instance.once("exit", () => { clearTimeout(killTimeout); resolve(); @@ -410,14 +569,15 @@ export async function stop(targetName: DownloadableEmulators): Promise { /** * @param targetName */ -export async function downloadIfNecessary(targetName: DownloadableEmulators): Promise { +export async function downloadIfNecessary( + targetName: DownloadableEmulators, +): Promise { const hasEmulator = fs.existsSync(getExecPath(targetName)); - if (hasEmulator) { - return; + if (!hasEmulator) { + await downloadEmulator(targetName); } - - await downloadEmulator(targetName); + return Commands[targetName]; } /** @@ -427,10 +587,15 @@ export async function downloadIfNecessary(targetName: DownloadableEmulators): Pr */ export async function start( targetName: DownloadableEmulators, - args: any, - extraEnv: NodeJS.ProcessEnv = {} + args: { + auto_download?: boolean; + port?: number; + host?: string; + [k: string]: any; + }, + extraEnv: Partial = {}, ): Promise { - const downloadDetails = DownloadDetails[targetName]; + const downloadDetails = getDownloadDetails(targetName); const emulator = get(targetName); const hasEmulator = fs.existsSync(getExecPath(targetName)); const logger = EmulatorLogger.forEmulator(targetName); @@ -439,8 +604,8 @@ export async function start( if (process.env.CI) { utils.logWarning( `It appears you are running in a CI environment. You can avoid downloading the ${Constants.description( - targetName - )} repeatedly by caching the ${downloadDetails.opts.cacheDir} directory.` + targetName, + )} repeatedly by caching the ${downloadDetails.opts.cacheDir} directory.`, ); } @@ -455,7 +620,7 @@ export async function start( logger.log( "DEBUG", - `Starting ${Constants.description(targetName)} with command ${JSON.stringify(command)}` + `Starting ${Constants.description(targetName)} with command ${JSON.stringify(command)}`, ); return _runBinary(emulator, command, extraEnv); } diff --git a/src/emulator/emulatorLogger.ts b/src/emulator/emulatorLogger.ts index 601892404c5..65d48dd1726 100644 --- a/src/emulator/emulatorLogger.ts +++ b/src/emulator/emulatorLogger.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as utils from "../utils"; import { logger } from "../logger"; @@ -13,8 +13,9 @@ import { LogData } from "./loggingEmulator"; * USER - logged by user code, always show to humans. * WARN - warnings from our code that humans need. * WARN_ONCE - warnings from our code that humans need, but only once per session. + * ERROR - error from our code that humans need. */ -type LogType = "DEBUG" | "INFO" | "BULLET" | "SUCCESS" | "USER" | "WARN" | "WARN_ONCE"; +type LogType = "DEBUG" | "INFO" | "BULLET" | "SUCCESS" | "USER" | "WARN" | "WARN_ONCE" | "ERROR"; const TYPE_VERBOSITY: { [type in LogType]: number } = { DEBUG: 0, @@ -24,22 +25,35 @@ const TYPE_VERBOSITY: { [type in LogType]: number } = { USER: 2, WARN: 2, WARN_ONCE: 2, + ERROR: 2, }; export enum Verbosity { DEBUG = 0, INFO = 1, QUIET = 2, + SILENT = 3, } +export type ExtensionLogInfo = { + ref?: string; + instanceId?: string; +}; export class EmulatorLogger { - static verbosity: Verbosity = Verbosity.DEBUG; + private static verbosity: Verbosity = Verbosity.DEBUG; static warnOnceCache = new Set(); - constructor(private data: LogData = {}) {} + constructor( + public readonly name: string, + private data: LogData = {}, + ) {} + + static setVerbosity(verbosity: Verbosity) { + EmulatorLogger.verbosity = verbosity; + } static forEmulator(emulator: Emulators) { - return new EmulatorLogger({ + return new EmulatorLogger(emulator, { metadata: { emulator: { name: emulator, @@ -48,15 +62,27 @@ export class EmulatorLogger { }); } - static forFunction(functionName: string) { - return new EmulatorLogger({ + static forFunction(functionName: string, extensionLogInfo?: ExtensionLogInfo): EmulatorLogger { + return new EmulatorLogger(Emulators.FUNCTIONS, { metadata: { emulator: { - name: "functions", + name: Emulators.FUNCTIONS, }, function: { name: functionName, }, + extension: extensionLogInfo, + }, + }); + } + + static forExtension(extensionLogInfo: ExtensionLogInfo): EmulatorLogger { + return new EmulatorLogger(Emulators.EXTENSIONS, { + metadata: { + emulator: { + name: Emulators.EXTENSIONS, + }, + extension: extensionLogInfo, }, }); } @@ -112,6 +138,9 @@ export class EmulatorLogger { case "SUCCESS": utils.logSuccess(text, "info", mergedData); break; + case "ERROR": + utils.logBullet(text, "error", mergedData); + break; } } @@ -164,26 +193,26 @@ export class EmulatorLogger { case "googleapis-network-access": this.log( "WARN", - `Google API requested!\n - URL: "${systemLog.data.href}"\n - Be careful, this may be a production service.` + `Google API requested!\n - URL: "${systemLog.data.href}"\n - Be careful, this may be a production service.`, ); break; case "unidentified-network-access": this.log( "WARN", - `External network resource requested!\n - URL: "${systemLog.data.href}"\n - Be careful, this may be a production service.` + `External network resource requested!\n - URL: "${systemLog.data.href}"\n - Be careful, this may be a production service.`, ); break; case "functions-config-missing-value": this.log( "WARN_ONCE", - `It looks like you're trying to access functions.config().${systemLog.data.key} but there is no value there. You can learn more about setting up config here: https://firebase.google.com/docs/functions/local-emulator` + `It looks like you're trying to access functions.config().${systemLog.data.key} but there is no value there. You can learn more about setting up config here: https://firebase.google.com/docs/functions/local-emulator`, ); break; case "non-default-admin-app-used": this.log( "WARN", `Non-default "firebase-admin" instance created!\n ` + - `- This instance will *not* be mocked and will access production resources.` + `- This instance will *not* be mocked and will access production resources.`, ); break; case "missing-module": @@ -195,27 +224,27 @@ export class EmulatorLogger { systemLog.data.isDev ? "development dependency" : "dependency" }. To fix this, run "npm install ${systemLog.data.isDev ? "--save-dev" : "--save"} ${ systemLog.data.name - }" in your functions directory.` + }" in your functions directory.`, ); break; case "uninstalled-module": this.log( "WARN", `The Cloud Functions emulator requires the module "${systemLog.data.name}" to be installed. This package is in your package.json, but it's not available. \ -You probably need to run "npm install" in your functions directory.` +You probably need to run "npm install" in your functions directory.`, ); break; case "out-of-date-module": this.log( "WARN", `The Cloud Functions emulator requires the module "${systemLog.data.name}" to be version >${systemLog.data.minVersion} so your version is too old. \ -You can probably fix this by running "npm install ${systemLog.data.name}@latest" in your functions directory.` +You can probably fix this by running "npm install ${systemLog.data.name}@latest" in your functions directory.`, ); break; case "missing-package-json": this.log( "WARN", - `The Cloud Functions directory you specified does not have a "package.json" file, so we can't load it.` + `The Cloud Functions directory you specified does not have a "package.json" file, so we can't load it.`, ); break; case "function-code-resolution-failed": @@ -226,12 +255,12 @@ You can probably fix this by running "npm install ${systemLog.data.name}@latest" } if (systemLog.data.isPotentially.typescript) { helper.push( - " - It appears your code is written in Typescript, which must be compiled before emulation." + " - It appears your code is written in Typescript, which must be compiled before emulation.", ); } if (systemLog.data.isPotentially.uncompiled) { helper.push( - ` - You may be able to run "npm run build" in your functions directory to resolve this.` + ` - You may be able to run "npm run build" in your functions directory to resolve this.`, ); } utils.logWarning(helper.join("\n"), "warn", this.data); @@ -253,7 +282,14 @@ You can probably fix this by running "npm install ${systemLog.data.name}@latest" * @param text * @param data */ - logLabeled(type: LogType, label: string, text: string): void { + logLabeled(type: LogType, text: string): void; + logLabeled(type: LogType, label: string, text: string): void; + logLabeled(type: LogType, labelOrText: string, text?: string): void { + let label = labelOrText; + if (text === undefined) { + text = label; + label = this.name; + } if (EmulatorLogger.shouldSupress(type)) { logger.debug(`[${label}] ${text}`); return; @@ -286,6 +322,9 @@ You can probably fix this by running "npm install ${systemLog.data.name}@latest" EmulatorLogger.warnOnceCache.add(text); } break; + case "ERROR": + utils.logLabeledError(label, text, "error", mergedData); + break; } } diff --git a/src/emulator/emulatorServer.ts b/src/emulator/emulatorServer.ts deleted file mode 100644 index 42dd929872f..00000000000 --- a/src/emulator/emulatorServer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { EmulatorInstance } from "./types"; -import { EmulatorRegistry } from "./registry"; -import * as portUtils from "./portUtils"; -import { FirebaseError } from "../error"; - -/** - * Wrapper object to expose an EmulatorInstance for "firebase serve" that - * also registers the emulator with the registry. - */ -export class EmulatorServer { - constructor(public instance: EmulatorInstance) {} - - async start(): Promise { - const { port, host } = this.instance.getInfo(); - const portOpen = await portUtils.checkPortOpen(port, host); - - if (!portOpen) { - throw new FirebaseError( - `Port ${port} is not open on ${host}, could not start ${this.instance.getName()} emulator.` - ); - } - - await EmulatorRegistry.start(this.instance); - } - - async connect(): Promise { - await this.instance.connect(); - } - - async stop(): Promise { - await EmulatorRegistry.stop(this.instance.getName()); - } - - get(): EmulatorInstance { - return this.instance; - } -} diff --git a/src/emulator/env.ts b/src/emulator/env.ts new file mode 100644 index 00000000000..56cfc7cdc0e --- /dev/null +++ b/src/emulator/env.ts @@ -0,0 +1,43 @@ +import { Constants } from "./constants"; +import { EmulatorInfo, Emulators } from "./types"; +import { formatHost } from "./functionsEmulatorShared"; + +/** + * Adds or replaces emulator-related env vars (for Admin SDKs, etc.). + * @param env a `process.env`-like object or Record to be modified + * @param emulators the emulator info to use + */ +export function setEnvVarsForEmulators( + env: Record, + emulators: EmulatorInfo[], +): void { + for (const emu of emulators) { + const host = formatHost(emu); + switch (emu.name) { + case Emulators.FIRESTORE: + env[Constants.FIRESTORE_EMULATOR_HOST] = host; + env[Constants.FIRESTORE_EMULATOR_ENV_ALT] = host; + break; + case Emulators.DATABASE: + env[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = host; + break; + case Emulators.STORAGE: + env[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = host; + // The protocol is required for the Google Cloud Storage Node.js Client SDK. + env[Constants.CLOUD_STORAGE_EMULATOR_HOST] = `http://${host}`; + break; + case Emulators.AUTH: + env[Constants.FIREBASE_AUTH_EMULATOR_HOST] = host; + break; + case Emulators.HUB: + env[Constants.FIREBASE_EMULATOR_HUB] = host; + break; + case Emulators.PUBSUB: + env[Constants.PUBSUB_EMULATOR_HOST] = host; + break; + case Emulators.EVENTARC: + env[Constants.CLOUD_EVENTARC_EMULATOR_HOST] = `http://${host}`; + break; + } + } +} diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts new file mode 100644 index 00000000000..3c561a8f3c5 --- /dev/null +++ b/src/emulator/eventarcEmulator.ts @@ -0,0 +1,188 @@ +import * as express from "express"; + +import { Constants } from "./constants"; +import { EmulatorInfo, EmulatorInstance, Emulators } from "./types"; +import { createDestroyer } from "../utils"; +import { EmulatorLogger } from "./emulatorLogger"; +import { EventTrigger } from "./functionsEmulatorShared"; +import { CloudEvent } from "./events/types"; +import { EmulatorRegistry } from "./registry"; +import { FirebaseError } from "../error"; +import { cloudEventFromProtoToJson } from "./eventarcEmulatorUtils"; + +interface CustomEventTrigger { + projectId: string; + triggerName: string; + eventTrigger: EventTrigger; +} + +interface RequestWithRawBody extends express.Request { + rawBody: Buffer; +} + +export interface EventarcEmulatorArgs { + port?: number; + host?: string; +} + +export class EventarcEmulator implements EmulatorInstance { + private destroyServer?: () => Promise; + + private logger = EmulatorLogger.forEmulator(Emulators.EVENTARC); + private customEvents: { [key: string]: CustomEventTrigger[] } = {}; + + constructor(private args: EventarcEmulatorArgs) {} + + createHubServer(): express.Application { + const registerTriggerRoute = `/emulator/v1/projects/:project_id/triggers/:trigger_name(*)`; + const registerTriggerHandler: express.RequestHandler = (req, res) => { + const projectId = req.params.project_id; + const triggerName = req.params.trigger_name; + if (!projectId || !triggerName) { + const error = "Missing project ID or trigger name."; + this.logger.log("ERROR", error); + res.status(400).send({ error }); + return; + } + const bodyString = (req as RequestWithRawBody).rawBody.toString(); + const substituted = bodyString.replaceAll("${PROJECT_ID}", projectId); + const body = JSON.parse(substituted); + const eventTrigger = body.eventTrigger as EventTrigger; + if (!eventTrigger) { + const error = `Missing event trigger for ${triggerName}.`; + this.logger.log("ERROR", error); + res.status(400).send({ error }); + return; + } + const key = `${eventTrigger.eventType}-${eventTrigger.channel}`; + this.logger.logLabeled( + "BULLET", + "eventarc", + `Registering custom event trigger for ${key} with trigger name ${triggerName}.`, + ); + const customEventTriggers = this.customEvents[key] || []; + customEventTriggers.push({ projectId, triggerName, eventTrigger }); + this.customEvents[key] = customEventTriggers; + res.status(200).send({ res: "OK" }); + }; + + const publishEventsRoute = `/projects/:project_id/locations/:location/channels/:channel::publishEvents`; + const publishEventsHandler: express.RequestHandler = (req, res) => { + const channel = `projects/${req.params.project_id}/locations/${req.params.location}/channels/${req.params.channel}`; + const body = JSON.parse((req as RequestWithRawBody).rawBody.toString()); + for (const event of body.events) { + if (!event.type) { + res.sendStatus(400); + return; + } + this.logger.log( + "INFO", + `Received custom event at channel ${channel}: ${JSON.stringify(event, null, 2)}`, + ); + this.triggerCustomEventFunction(channel, event); + } + res.sendStatus(200); + }; + + const dataMiddleware: express.RequestHandler = (req, _, next) => { + const chunks: Buffer[] = []; + req.on("data", (chunk: Buffer) => { + chunks.push(chunk); + }); + req.on("end", () => { + (req as RequestWithRawBody).rawBody = Buffer.concat(chunks); + next(); + }); + }; + + const hub = express(); + hub.post([registerTriggerRoute], dataMiddleware, registerTriggerHandler); + hub.post([publishEventsRoute], dataMiddleware, publishEventsHandler); + hub.all("*", (req, res) => { + this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); + res.sendStatus(404); + }); + return hub; + } + + async triggerCustomEventFunction(channel: string, event: CloudEvent) { + if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { + this.logger.log("INFO", "Functions emulator not found. This should not happen."); + return Promise.reject(); + } + const key = `${event.type}-${channel}`; + const triggers = this.customEvents[key] || []; + return await Promise.all( + triggers + .filter( + (trigger) => + !trigger.eventTrigger.eventFilters || + this.matchesAll(event, trigger.eventTrigger.eventFilters), + ) + .map((trigger) => + EmulatorRegistry.client(Emulators.FUNCTIONS) + .request, NodeJS.ReadableStream>({ + method: "POST", + path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, + body: JSON.stringify(cloudEventFromProtoToJson(event)), + responseType: "stream", + resolveOnHTTPError: true, + }) + .then((res) => { + // Since the response type is a stream and using `resolveOnHTTPError: true`, we check status manually. + if (res.status >= 400) { + throw new FirebaseError(`Received non-200 status code: ${res.status}`); + } + }) + .catch((err) => { + this.logger.log( + "ERROR", + `Failed to trigger Functions emulator for ${trigger.triggerName}: ${err}`, + ); + }), + ), + ); + } + + private matchesAll(event: CloudEvent, eventFilters: Record): boolean { + return Object.entries(eventFilters).every(([key, value]) => { + let attr = event[key] ?? event.attributes[key]; + if (typeof attr === "object" && !Array.isArray(attr)) { + attr = attr.ceTimestamp ?? attr.ceString; + } + return attr === value; + }); + } + + async start(): Promise { + const { host, port } = this.getInfo(); + const server = this.createHubServer().listen(port, host); + this.destroyServer = createDestroyer(server); + return Promise.resolve(); + } + + async connect(): Promise { + return Promise.resolve(); + } + + async stop(): Promise { + if (this.destroyServer) { + await this.destroyServer(); + } + } + + getInfo(): EmulatorInfo { + const host = this.args.host || Constants.getDefaultHost(); + const port = this.args.port || Constants.getDefaultPort(Emulators.EVENTARC); + + return { + name: this.getName(), + host, + port, + }; + } + + getName(): Emulators { + return Emulators.EVENTARC; + } +} diff --git a/src/emulator/eventarcEmulatorUtils.spec.ts b/src/emulator/eventarcEmulatorUtils.spec.ts new file mode 100644 index 00000000000..921dcb28b7e --- /dev/null +++ b/src/emulator/eventarcEmulatorUtils.spec.ts @@ -0,0 +1,162 @@ +import { expect } from "chai"; + +import { cloudEventFromProtoToJson } from "./eventarcEmulatorUtils"; + +describe("eventarcEmulatorUtils", () => { + describe("cloudEventFromProtoToJson", () => { + it("converts cloud event from proto format", () => { + expect( + cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }), + ).to.deep.eq({ + type: "some.custom.event", + specversion: "1.0", + subject: "context", + datacontenttype: "application/json", + id: "user-provided-id", + data: { + hello: "world", + }, + source: "/my/functions", + time: "2022-03-16T20:20:42.212Z", + customattr: "custom value", + }); + }); + + it("throws invalid argument when source not set", () => { + expect(() => + cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }), + ).throws("CloudEvent 'source' is required."); + }); + + it("populates converts object data to JSON and sets datacontenttype", () => { + const got = cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }); + expect(got.datacontenttype).to.deep.eq("application/json"); + expect(got.data).to.deep.eq({ hello: "world" }); + }); + + it("populates string data and sets datacontenttype", () => { + const got = cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "text/plain", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: "hello world", + type: "some.custom.event", + }); + expect(got.datacontenttype).to.deep.eq("text/plain"); + expect(got.data).to.eq("hello world"); + }); + + it("allows optional attribute to not be set", () => { + expect( + cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }), + ).to.deep.eq({ + type: "some.custom.event", + specversion: "1.0", + datacontenttype: "application/json", + id: "user-provided-id", + subject: undefined, + data: { + hello: "world", + }, + source: "/my/functions", + time: "2022-03-16T20:20:42.212Z", + customattr: "custom value", + }); + }); + }); +}); diff --git a/src/emulator/eventarcEmulatorUtils.ts b/src/emulator/eventarcEmulatorUtils.ts new file mode 100644 index 00000000000..4b3b631c62b --- /dev/null +++ b/src/emulator/eventarcEmulatorUtils.ts @@ -0,0 +1,62 @@ +import { CloudEvent } from "./events/types"; +import { FirebaseError } from "../error"; + +const BUILT_IN_ATTRS: string[] = ["time", "datacontenttype", "subject"]; + +export function cloudEventFromProtoToJson(ce: any): CloudEvent { + if (ce["id"] === undefined) { + throw new FirebaseError("CloudEvent 'id' is required."); + } + if (ce["type"] === undefined) { + throw new FirebaseError("CloudEvent 'type' is required."); + } + if (ce["specVersion"] === undefined) { + throw new FirebaseError("CloudEvent 'specVersion' is required."); + } + if (ce["source"] === undefined) { + throw new FirebaseError("CloudEvent 'source' is required."); + } + const out: CloudEvent = { + id: ce["id"], + type: ce["type"], + specversion: ce["specVersion"], + source: ce["source"], + subject: getOptionalAttribute(ce, "subject", "ceString"), + time: getRequiredAttribute(ce, "time", "ceTimestamp"), + data: getData(ce), + datacontenttype: getRequiredAttribute(ce, "datacontenttype", "ceString"), + }; + for (const attr in ce["attributes"]) { + if (BUILT_IN_ATTRS.includes(attr)) { + continue; + } + out[attr] = getRequiredAttribute(ce, attr, "ceString"); + } + return out; +} + +function getOptionalAttribute(ce: any, attr: string, type: string): string | undefined { + return ce?.["attributes"]?.[attr]?.[type]; +} + +function getRequiredAttribute(ce: any, attr: string, type: string): string { + const val = ce?.["attributes"]?.[attr]?.[type]; + if (val === undefined) { + throw new FirebaseError("CloudEvent must contain " + attr + " attribute"); + } + return val; +} + +function getData(ce: any): any { + const contentType = getRequiredAttribute(ce, "datacontenttype", "ceString"); + switch (contentType) { + case "application/json": + return JSON.parse(ce["textData"]); + case "text/plain": + return ce["textData"]; + case undefined: + return undefined; + default: + throw new FirebaseError("Unsupported content type: " + contentType); + } +} diff --git a/src/emulator/events/types.ts b/src/emulator/events/types.ts index 4119607ed50..17f14c940d7 100644 --- a/src/emulator/events/types.ts +++ b/src/emulator/events/types.ts @@ -5,9 +5,6 @@ * * We can't import some of them because they are marked "internal". */ - -import * as _ from "lodash"; - import { Resource } from "firebase-functions"; import * as express from "express"; @@ -78,6 +75,14 @@ export interface CloudEvent { * actually receives. */ params?: Record; + + /** + * The type of data that has been passed, e.g. application/json. + */ + datacontenttype?: string; + + /** Custom attributes. */ + [key: string]: any; } export type CloudEventContext = Omit, "data" | "params">; @@ -90,16 +95,28 @@ export interface AuthMode { variable?: any; } +export type AuthType = "USER" | "ADMIN" | "UNAUTHENTICATED"; + +export interface EventOptions { + params?: Record; + authType?: AuthType; + auth?: Partial & { + uid?: string; + token?: string; + }; + resource?: string | { name: string; service: string }; +} + /** * Utilities for operating on event types. */ export class EventUtils { static isEvent(proto: any): proto is Event { - return _.has(proto, "context") && _.has(proto, "data"); + return proto.context && proto.data; } static isLegacyEvent(proto: any): proto is LegacyEvent { - return _.has(proto, "data") && _.has(proto, "resource"); + return proto.data && proto.resource; } static isBinaryCloudEvent(req: express.Request): boolean { diff --git a/src/emulator/extensions/postinstall.spec.ts b/src/emulator/extensions/postinstall.spec.ts new file mode 100644 index 00000000000..b2f370cd7ab --- /dev/null +++ b/src/emulator/extensions/postinstall.spec.ts @@ -0,0 +1,88 @@ +import { expect } from "chai"; +import * as postinstall from "./postinstall"; +import { EmulatorRegistry } from "../registry"; +import { Emulators } from "../types"; +import { FakeEmulator } from "../testing/fakeEmulator"; + +describe("replaceConsoleLinks", () => { + let host: string; + let port: number; + before(async () => { + const emu = await FakeEmulator.create(Emulators.UI); + host = emu.getInfo().host; + port = emu.getInfo().port; + return EmulatorRegistry.start(emu); + }); + + after(async () => { + await EmulatorRegistry.stopAll(); + }); + + const tests: { + desc: string; + input: string; + expected: () => string; + }[] = [ + { + desc: "should replace Firestore links", + input: + " Go to your [Cloud Firestore dashboard](https://console.firebase.google.com/project/test-project/firestore/data) in the Firebase console.", + expected: () => + ` Go to your [Cloud Firestore dashboard](http://${host}:${port}/firestore) in the Firebase console.`, + }, + { + desc: "should replace Functions links", + input: + " Go to your [Cloud Functions dashboard](https://console.firebase.google.com/project/test-project/functions/logs) in the Firebase console.", + expected: () => + ` Go to your [Cloud Functions dashboard](http://${host}:${port}/logs) in the Firebase console.`, + }, + { + desc: "should replace Extensions links", + input: + " Go to your [Extensions dashboard](https://console.firebase.google.com/project/test-project/extensions) in the Firebase console.", + expected: () => + ` Go to your [Extensions dashboard](http://${host}:${port}/extensions) in the Firebase console.`, + }, + { + desc: "should replace RTDB links", + input: + " Go to your [Realtime database dashboard](https://console.firebase.google.com/project/test-project/database/test-walkthrough/data) in the Firebase console.", + expected: () => + ` Go to your [Realtime database dashboard](http://${host}:${port}/database) in the Firebase console.`, + }, + { + desc: "should replace Auth links", + input: + " Go to your [Auth dashboard](https://console.firebase.google.com/project/test-project/authentication/users) in the Firebase console.", + expected: () => + ` Go to your [Auth dashboard](http://${host}:${port}/auth) in the Firebase console.`, + }, + { + desc: "should replace multiple GAIA user links ", + input: + " Go to your [Auth dashboard](https://console.firebase.google.com/u/0/project/test-project/authentication/users) in the Firebase console.", + expected: () => + ` Go to your [Auth dashboard](http://${host}:${port}/auth) in the Firebase console.`, + }, + { + desc: "should replace multiple links", + input: + " Go to your [Cloud Firestore dashboard](https://console.firebase.google.com/project/jh-walkthrough/firestore/data) or [Realtime database dashboard](https://console.firebase.google.com/project/test-project/database/test-walkthrough/data)in the Firebase console.", + expected: () => + ` Go to your [Cloud Firestore dashboard](http://${host}:${port}/firestore) or [Realtime database dashboard](http://${host}:${port}/database)in the Firebase console.`, + }, + { + desc: "should not replace other links", + input: " Go to your [Stripe dashboard](https://stripe.com/payments) to see more information.", + expected: () => + " Go to your [Stripe dashboard](https://stripe.com/payments) to see more information.", + }, + ]; + + for (const t of tests) { + it(t.desc, () => { + expect(postinstall.replaceConsoleLinks(t.input)).to.equal(t.expected()); + }); + } +}); diff --git a/src/emulator/extensions/postinstall.ts b/src/emulator/extensions/postinstall.ts new file mode 100644 index 00000000000..a4f9fb2a20f --- /dev/null +++ b/src/emulator/extensions/postinstall.ts @@ -0,0 +1,42 @@ +import { EmulatorRegistry } from "../registry"; +import { Emulators } from "../types"; + +/** + * replaceConsoleLinks replaces links to production Firebase console with links to the corresponding Emulator UI page. + * @param postinstall The postinstall instructions to check for console links. + */ +export function replaceConsoleLinks(postinstall: string): string { + const uiRunning = EmulatorRegistry.isRunning(Emulators.UI); + const uiUrl = uiRunning ? EmulatorRegistry.url(Emulators.UI).toString() : "unknown"; + let subbedPostinstall = postinstall; + const linkReplacements = new Map([ + [ + /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/storage[A-Za-z0-9\/-]*(?=[\)\]\s])/, + `${uiUrl}${Emulators.STORAGE}`, + ], // Storage console links + [ + /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/firestore[A-Za-z0-9\/-]*(?=[\)\]\s])/, + `${uiUrl}${Emulators.FIRESTORE}`, + ], // Firestore console links + [ + /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/database[A-Za-z0-9\/-]*(?=[\)\]\s])/, + `${uiUrl}${Emulators.DATABASE}`, + ], // RTDB console links + [ + /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/authentication[A-Za-z0-9\/-]*(?=[\)\]\s])/, + `${uiUrl}${Emulators.AUTH}`, + ], // Auth console links + [ + /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/functions[A-Za-z0-9\/-]*(?=[\)\]\s])/, + `${uiUrl}logs`, // There is no functions page in the UI, so redirect to logs. + ], // Functions console links + [ + /(http[s]?:\/\/)?console\.firebase\.google\.com\/(u\/[0-9]\/)?project\/[A-Za-z0-9-]+\/extensions[A-Za-z0-9\/-]*(?=[\)\]\s])/, + `${uiUrl}${Emulators.EXTENSIONS}`, + ], // Extensions console links + ]); + for (const [consoleLinkRegex, replacement] of linkReplacements) { + subbedPostinstall = subbedPostinstall.replace(consoleLinkRegex, replacement); + } + return subbedPostinstall; +} diff --git a/src/emulator/extensions/validation.spec.ts b/src/emulator/extensions/validation.spec.ts new file mode 100644 index 00000000000..5e4a95c9a0c --- /dev/null +++ b/src/emulator/extensions/validation.spec.ts @@ -0,0 +1,257 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as validation from "./validation"; +import * as ensureApiEnabled from "../../ensureApiEnabled"; +import * as controller from "../controller"; +import { DeploymentInstanceSpec } from "../../deploy/extensions/planner"; +import { EmulatableBackend } from "../functionsEmulator"; +import { Emulators } from "../types"; +import { EventTrigger, ParsedTriggerDefinition } from "../functionsEmulatorShared"; +import { Options } from "../../options"; +import { RC } from "../../rc"; +import { Config } from "../../config"; + +const TEST_OPTIONS: Options = { + cwd: ".", + configPath: ".", + only: "", + except: "", + force: false, + filteredTargets: [""], + nonInteractive: true, + interactive: false, + json: false, + debug: false, + rc: new RC(), + config: new Config("."), +}; +function fakeInstanceSpecWithAPI(instanceId: string, apiName: string): DeploymentInstanceSpec { + return { + instanceId, + params: {}, + systemParams: {}, + ref: { + publisherId: "test", + extensionId: "test", + version: "0.1.0", + }, + extensionVersion: { + name: "publishers/test/extensions/test/versions/0.1.0", + ref: "test/test@0.1.0", + state: "PUBLISHED", + sourceDownloadUri: "test.com", + hash: "abc123", + spec: { + name: "test", + version: "0.1.0", + sourceUrl: "test.com", + resources: [], + params: [], + systemParams: [], + apis: [{ apiName, reason: "because" }], + }, + }, + }; +} + +function getTestEmulatableBackend( + predefinedTriggers: ParsedTriggerDefinition[], +): EmulatableBackend { + return { + functionsDir: ".", + env: {}, + secretEnv: [], + codebase: "", + predefinedTriggers, + }; +} + +function getTestParsedTriggerDefinition(args: { + httpsTrigger?: {}; + eventTrigger?: EventTrigger; +}): ParsedTriggerDefinition { + return { + entryPoint: "test", + platform: "gcfv1", + name: "test", + eventTrigger: args.eventTrigger, + httpsTrigger: args.httpsTrigger, + }; +} + +describe("ExtensionsEmulator validation", () => { + describe(`${validation.getUnemulatedAPIs.name}`, () => { + const testProjectId = "test-project"; + const testAPI = "test.googleapis.com"; + const sandbox = sinon.createSandbox(); + let checkStub: sinon.SinonStub; + + beforeEach(() => { + checkStub = sandbox.stub(ensureApiEnabled, "check"); + checkStub.withArgs(testProjectId, testAPI, "extensions", true).resolves(true); + checkStub.throws("Unexpected API checked in test"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should check only unemulated APIs", async () => { + const instanceIdWithUnemulatedAPI = "unemulated"; + const instanceId2WithUnemulatedAPI = "unemulated2"; + const instanceIdWithEmulatedAPI = "emulated"; + + const result = await validation.getUnemulatedAPIs(testProjectId, [ + fakeInstanceSpecWithAPI(instanceIdWithEmulatedAPI, "firestore.googleapis.com"), + fakeInstanceSpecWithAPI(instanceIdWithUnemulatedAPI, testAPI), + fakeInstanceSpecWithAPI(instanceId2WithUnemulatedAPI, testAPI), + ]); + + expect(result).to.deep.equal([ + { + apiName: testAPI, + instanceIds: [instanceIdWithUnemulatedAPI, instanceId2WithUnemulatedAPI], + enabled: true, + }, + ]); + }); + + it("should not check on demo- projects", async () => { + const instanceIdWithUnemulatedAPI = "unemulated"; + const instanceId2WithUnemulatedAPI = "unemulated2"; + const instanceIdWithEmulatedAPI = "emulated"; + + const result = await validation.getUnemulatedAPIs(`demo-${testProjectId}`, [ + fakeInstanceSpecWithAPI(instanceIdWithEmulatedAPI, "firestore.googleapis.com"), + fakeInstanceSpecWithAPI(instanceIdWithUnemulatedAPI, testAPI), + fakeInstanceSpecWithAPI(instanceId2WithUnemulatedAPI, testAPI), + ]); + + expect(result).to.deep.equal([ + { + apiName: testAPI, + instanceIds: [instanceIdWithUnemulatedAPI, instanceId2WithUnemulatedAPI], + enabled: false, + }, + ]); + expect(checkStub.callCount).to.equal(0); + }); + }); + + describe(`${validation.checkForUnemulatedTriggerTypes.name}`, () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + const shouldStartStub = sandbox.stub(controller, "shouldStart"); + shouldStartStub.withArgs(sinon.match.any, Emulators.STORAGE).returns(true); + shouldStartStub.withArgs(sinon.match.any, Emulators.DATABASE).returns(true); + shouldStartStub.withArgs(sinon.match.any, Emulators.EVENTARC).returns(true); + shouldStartStub.withArgs(sinon.match.any, Emulators.FIRESTORE).returns(false); + shouldStartStub.withArgs(sinon.match.any, Emulators.AUTH).returns(false); + }); + + afterEach(() => { + sandbox.restore(); + }); + + const tests: { + desc: string; + input: ParsedTriggerDefinition[]; + want: string[]; + }[] = [ + { + desc: "should return trigger types for emulators that are not running", + input: [ + getTestParsedTriggerDefinition({ + eventTrigger: { + resource: "test/{*}", + eventType: "providers/cloud.firestore/eventTypes/document.create", + }, + }), + getTestParsedTriggerDefinition({ + eventTrigger: { + resource: "test", + eventType: "providers/firebase.auth/eventTypes/user.create", + }, + }), + ], + want: ["firestore", "auth"], + }, + { + desc: "should return trigger types that don't have an emulator", + input: [ + getTestParsedTriggerDefinition({ + eventTrigger: { + resource: "test", + eventType: "providers/google.firebase.analytics/eventTypes/event.log", + }, + }), + ], + want: ["analytics"], + }, + { + desc: "should not return duplicates", + input: [ + getTestParsedTriggerDefinition({ + eventTrigger: { + resource: "test/{*}", + eventType: "providers/cloud.firestore/eventTypes/document.create", + }, + }), + getTestParsedTriggerDefinition({ + eventTrigger: { + resource: "test/{*}", + eventType: "providers/cloud.firestore/eventTypes/document.create", + }, + }), + ], + want: ["firestore"], + }, + { + desc: "should not return trigger types for emulators that are running", + input: [ + getTestParsedTriggerDefinition({ + eventTrigger: { + resource: "test/{*}", + eventType: "google.storage.object.finalize", + }, + }), + getTestParsedTriggerDefinition({ + eventTrigger: { + resource: "test/{*}", + eventType: "providers/google.firebase.database/eventTypes/ref.write", + }, + }), + getTestParsedTriggerDefinition({ + eventTrigger: { + eventType: "test.custom.event", + channel: "projects/foo/locations/us-central1/channels/firebase", + }, + }), + ], + want: [], + }, + { + desc: "should not return trigger types for https triggers", + input: [ + getTestParsedTriggerDefinition({ + httpsTrigger: {}, + }), + ], + want: [], + }, + ]; + + for (const test of tests) { + it(test.desc, () => { + const result = validation.checkForUnemulatedTriggerTypes( + getTestEmulatableBackend(test.input), + TEST_OPTIONS, + ); + + expect(result).to.have.members(test.want); + }); + } + }); +}); diff --git a/src/emulator/extensions/validation.ts b/src/emulator/extensions/validation.ts new file mode 100644 index 00000000000..7d0764ff9d2 --- /dev/null +++ b/src/emulator/extensions/validation.ts @@ -0,0 +1,93 @@ +import * as planner from "../../deploy/extensions/planner"; +import { shouldStart } from "../controller"; +import { Constants } from "../constants"; +import { check } from "../../ensureApiEnabled"; +import { getFunctionService } from "../functionsEmulatorShared"; +import { EmulatableBackend } from "../functionsEmulator"; +import { ParsedTriggerDefinition } from "../functionsEmulatorShared"; +import { Emulators } from "../types"; +import { Options } from "../../options"; + +const EMULATED_APIS = [ + "storage-component.googleapis.com", + "firestore.googleapis.com", + "pubsub.googleapis.com", + "identitytoolkit.googleapis.com", + // TODO: Is there a RTDB API we need to add here? I couldn't find one. +]; + +type APIInfo = { + apiName: string; + instanceIds: string[]; + enabled: boolean; +}; +/** + * getUnemulatedAPIs checks a list of InstanceSpecs for APIs that are not emulated. + * It returns a map of API name to list of instanceIds that use that API. + */ +export async function getUnemulatedAPIs( + projectId: string, + instances: planner.InstanceSpec[], +): Promise { + const unemulatedAPIs: Record = {}; + for (const i of instances) { + const extensionSpec = await planner.getExtensionSpec(i); + for (const api of extensionSpec.apis ?? []) { + if (!EMULATED_APIS.includes(api.apiName)) { + if (unemulatedAPIs[api.apiName]) { + unemulatedAPIs[api.apiName].instanceIds.push(i.instanceId); + } else { + const enabled = + !Constants.isDemoProject(projectId) && + (await check(projectId, api.apiName, "extensions", true)); + unemulatedAPIs[api.apiName] = { + apiName: api.apiName, + instanceIds: [i.instanceId], + enabled, + }; + } + } + } + } + return Object.values(unemulatedAPIs); +} + +/** + * Checks a EmulatableBackend for any functions that trigger off of emulators that are not running or not implemented. + * @param backend + */ +export function checkForUnemulatedTriggerTypes( + backend: EmulatableBackend, + options: Options, +): string[] { + const triggers = backend.predefinedTriggers ?? []; + const unemulatedTriggers = triggers + .filter((definition: ParsedTriggerDefinition) => { + if (definition.httpsTrigger) { + // HTTPS triggers can always be emulated. + return false; + } + if (definition.eventTrigger) { + const service: string = getFunctionService(definition); + switch (service) { + case Constants.SERVICE_FIRESTORE: + return !shouldStart(options, Emulators.FIRESTORE); + case Constants.SERVICE_REALTIME_DATABASE: + return !shouldStart(options, Emulators.DATABASE); + case Constants.SERVICE_PUBSUB: + return !shouldStart(options, Emulators.PUBSUB); + case Constants.SERVICE_AUTH: + return !shouldStart(options, Emulators.AUTH); + case Constants.SERVICE_STORAGE: + return !shouldStart(options, Emulators.STORAGE); + case Constants.SERVICE_EVENTARC: + return !shouldStart(options, Emulators.EVENTARC); + default: + return true; + } + } + }) + .map((definition) => Constants.getServiceName(getFunctionService(definition))); + // Remove duplicates + return [...new Set(unemulatedTriggers)]; +} diff --git a/src/emulator/extensionsEmulator.spec.ts b/src/emulator/extensionsEmulator.spec.ts new file mode 100644 index 00000000000..a24a945d998 --- /dev/null +++ b/src/emulator/extensionsEmulator.spec.ts @@ -0,0 +1,146 @@ +import { expect } from "chai"; +import { join } from "node:path"; + +import * as planner from "../deploy/extensions/planner"; +import { ExtensionsEmulator } from "./extensionsEmulator"; +import { EmulatableBackend } from "./functionsEmulator"; +import { Extension, ExtensionVersion, RegistryLaunchStage, Visibility } from "../extensions/types"; + +const TEST_EXTENSION: Extension = { + name: "publishers/firebase/extensions/storage-resize-images", + ref: "firebase/storage-resize-images", + visibility: Visibility.PUBLIC, + state: "PUBLISHED", + registryLaunchStage: RegistryLaunchStage.BETA, + createTime: "0", +}; + +const TEST_EXTENSION_VERSION: ExtensionVersion = { + name: "publishers/firebase/extensions/storage-resize-images/versions/0.1.18", + ref: "firebase/storage-resize-images@0.1.18", + state: "PUBLISHED", + sourceDownloadUri: "https://fake.test", + hash: "abc123", + spec: { + name: "publishers/firebase/extensions/storage-resize-images/versions/0.1.18", + resources: [ + { + type: "firebaseextensions.v1beta.function", + name: "generateResizedImage", + description: `Listens for new images uploaded to your specified Cloud Storage bucket, resizes the images, + then stores the resized images in the same bucket. Optionally keeps or deletes the original images.`, + properties: { + location: "${param:LOCATION}", + runtime: "nodejs10", + eventTrigger: { + eventType: "google.storage.object.finalize", + resource: "projects/_/buckets/${param:IMG_BUCKET}", + }, + }, + }, + ], + params: [], + systemParams: [], + version: "0.1.18", + sourceUrl: "https://fake.test", + }, +}; + +describe("Extensions Emulator", () => { + describe("toEmulatableBackends", () => { + let previousCachePath: string | undefined; + beforeEach(() => { + previousCachePath = process.env.FIREBASE_EXTENSIONS_CACHE_PATH; + process.env.FIREBASE_EXTENSIONS_CACHE_PATH = "./src/test/emulators/extensions"; + }); + afterEach(() => { + process.env.FIREBASE_EXTENSIONS_CACHE_PATH = previousCachePath; + }); + const testCases: { + desc: string; + input: planner.DeploymentInstanceSpec; + expected: EmulatableBackend; + }[] = [ + { + desc: "should transform a instance spec to a backend", + input: { + instanceId: "ext-test", + ref: { + publisherId: "firebase", + extensionId: "storage-resize-images", + version: "0.1.18", + }, + params: { + LOCATION: "us-west1", + ALLOWED_EVENT_TYPES: + "google.firebase.image-resize-started,google.firebase.image-resize-completed", + EVENTARC_CHANNEL: "projects/test-project/locations/us-central1/channels/firebase", + }, + systemParams: {}, + allowedEventTypes: [ + "google.firebase.image-resize-started", + "google.firebase.image-resize-completed", + ], + eventarcChannel: "projects/test-project/locations/us-central1/channels/firebase", + extension: TEST_EXTENSION, + extensionVersion: TEST_EXTENSION_VERSION, + }, + expected: { + env: { + LOCATION: "us-west1", + DATABASE_INSTANCE: "test-project", + DATABASE_URL: "https://test-project.firebaseio.com", + EXT_INSTANCE_ID: "ext-test", + PROJECT_ID: "test-project", + STORAGE_BUCKET: "test-project.appspot.com", + ALLOWED_EVENT_TYPES: + "google.firebase.image-resize-started,google.firebase.image-resize-completed", + EVENTARC_CHANNEL: "projects/test-project/locations/us-central1/channels/firebase", + EVENTARC_CLOUD_EVENT_SOURCE: "projects/test-project/instances/ext-test", + }, + secretEnv: [], + extensionInstanceId: "ext-test", + // use join to convert path to platform dependent path + // so test also runs on win machines + // eslint-disable-next-line prettier/prettier + functionsDir: join( + "src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions", + ), + runtime: "nodejs10", + predefinedTriggers: [ + { + entryPoint: "generateResizedImage", + eventTrigger: { + eventType: "google.storage.object.finalize", + resource: "projects/_/buckets/${param:IMG_BUCKET}", + service: "storage.googleapis.com", + }, + name: "ext-ext-test-generateResizedImage", + platform: "gcfv1", + regions: ["us-west1"], + }, + ], + extension: TEST_EXTENSION, + extensionVersion: TEST_EXTENSION_VERSION, + codebase: "ext-test", + }, + }, + ]; + for (const testCase of testCases) { + it(testCase.desc, async () => { + const e = new ExtensionsEmulator({ + projectId: "test-project", + projectNumber: "1234567", + projectDir: ".", + extensions: {}, + aliases: [], + }); + + const result = await e.toEmulatableBackend(testCase.input); + // ignore result.bin, as it is platform dependent + delete result.bin; + expect(result).to.deep.equal(testCase.expected); + }); + } + }); +}); diff --git a/src/emulator/extensionsEmulator.ts b/src/emulator/extensionsEmulator.ts new file mode 100644 index 00000000000..8f24338b5e9 --- /dev/null +++ b/src/emulator/extensionsEmulator.ts @@ -0,0 +1,374 @@ +import * as clc from "colorette"; +import * as spawn from "cross-spawn"; +import * as fs from "fs-extra"; +import * as os from "os"; +import * as path from "path"; +const Table = require("cli-table"); + +import * as planner from "../deploy/extensions/planner"; +import { enableApiURI } from "../ensureApiEnabled"; +import { FirebaseError } from "../error"; +import { getExtensionFunctionInfo } from "../extensions/emulator/optionsHelper"; +import { toExtensionVersionRef } from "../extensions/refs"; +import { Options } from "../options"; +import { shortenUrl } from "../shortenUrl"; +import { Constants } from "./constants"; +import { downloadExtensionVersion } from "./download"; +import { EmulatorLogger } from "./emulatorLogger"; +import { checkForUnemulatedTriggerTypes, getUnemulatedAPIs } from "./extensions/validation"; +import { EmulatableBackend } from "./functionsEmulator"; +import { EmulatorRegistry } from "./registry"; +import { EmulatorInfo, EmulatorInstance, Emulators } from "./types"; + +export interface ExtensionEmulatorArgs { + projectId: string; + projectNumber: string; + aliases?: string[]; + extensions: Record; + projectDir: string; +} + +export class ExtensionsEmulator implements EmulatorInstance { + private want: planner.DeploymentInstanceSpec[] = []; + private backends: EmulatableBackend[] = []; + private args: ExtensionEmulatorArgs; + private logger = EmulatorLogger.forEmulator(Emulators.EXTENSIONS); + + // Keeps track of all the extension sources that are being downloaded. + private pendingDownloads = new Map>(); + + constructor(args: ExtensionEmulatorArgs) { + this.args = args; + } + + public start(): Promise { + this.logger.logLabeled("DEBUG", "Extensions", "Started Extensions emulator, this is a noop."); + return Promise.resolve(); + } + + public stop(): Promise { + this.logger.logLabeled("DEBUG", "Extensions", "Stopping Extensions emulator, this is a noop."); + return Promise.resolve(); + } + + public connect(): Promise { + this.logger.logLabeled( + "DEBUG", + "Extensions", + "Connecting Extensions emulator, this is a noop.", + ); + return Promise.resolve(); + } + + public getInfo(): EmulatorInfo { + const functionsEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); + if (!functionsEmulator) { + throw new FirebaseError( + "Extensions Emulator is running but Functions emulator is not. This should never happen.", + ); + } + return { ...functionsEmulator.getInfo(), name: this.getName() }; + } + + public getName(): Emulators { + return Emulators.EXTENSIONS; + } + + // readManifest checks the `extensions` section of `firebase.json` for the extension instances to emulate, + // and the `{projectRoot}/extensions` directory for param values. + private async readManifest(): Promise { + this.want = await planner.want({ + projectId: this.args.projectId, + projectNumber: this.args.projectNumber, + aliases: this.args.aliases ?? [], + projectDir: this.args.projectDir, + extensions: this.args.extensions, + emulatorMode: true, + }); + } + + // ensureSourceCode checks the cache for the source code for a given extension version, + // downloads and builds it if it is not found, then returns the path to that source code. + private async ensureSourceCode(instance: planner.InstanceSpec): Promise { + if (instance.localPath) { + if (!this.hasValidSource({ path: instance.localPath, extTarget: instance.localPath })) { + throw new FirebaseError( + `Tried to emulate local extension at ${instance.localPath}, but it was missing required files.`, + ); + } + return path.resolve(instance.localPath); + } else if (instance.ref) { + const ref = toExtensionVersionRef(instance.ref); + const cacheDir = + process.env.FIREBASE_EXTENSIONS_CACHE_PATH || + path.join(os.homedir(), ".cache", "firebase", "extensions"); + const sourceCodePath = path.join(cacheDir, ref); + + // Wait for previous download promise to resolve before we check source validity. + // This avoids racing to download the same source multiple times. + // Note: The below will not work because it throws the thread to the back of the message queue. + // await (this.pendingDownloads.get(ref) ?? Promise.resolve()); + if (this.pendingDownloads.get(ref)) { + await this.pendingDownloads.get(ref); + } + + if (!this.hasValidSource({ path: sourceCodePath, extTarget: ref })) { + const promise = this.downloadSource(instance, ref, sourceCodePath); + this.pendingDownloads.set(ref, promise); + await promise; + } + return sourceCodePath; + } else { + throw new FirebaseError( + "Tried to emulate an extension instance without a ref or localPath. This should never happen.", + ); + } + } + + private async downloadSource( + instance: planner.InstanceSpec, + ref: string, + sourceCodePath: string, + ): Promise { + const extensionVersion = await planner.getExtensionVersion(instance); + await downloadExtensionVersion(ref, extensionVersion.sourceDownloadUri, sourceCodePath); + this.installAndBuildSourceCode(sourceCodePath); + } + + /** + * Returns if the source code at given path is valid. + * + * Checks against a list of required files or directories that need to be present. + */ + private hasValidSource(args: { path: string; extTarget: string }): boolean { + // TODO(lihes): Source code can technically exist in other than "functions" dir. + // https://source.corp.google.com/piper///depot/google3/firebase/mods/go/worker/fetch_mod_source.go;l=451 + const requiredFiles = ["./extension.yaml", "./functions/package.json"]; + // If the directory isn't found, no need to check for files or print errors. + if (!fs.existsSync(args.path)) { + return false; + } + for (const requiredFile of requiredFiles) { + const f = path.join(args.path, requiredFile); + if (!fs.existsSync(f)) { + this.logger.logLabeled( + "BULLET", + "extensions", + `Detected invalid source code for ${args.extTarget}, expected to find ${f}`, + ); + return false; + } + } + this.logger.logLabeled("DEBUG", "extensions", `Source code valid for ${args.extTarget}`); + return true; + } + + installAndBuildSourceCode(sourceCodePath: string): void { + // TODO: Add logging during this so it is clear what is happening. + this.logger.logLabeled("DEBUG", "Extensions", `Running "npm install" for ${sourceCodePath}`); + const functionsDirectory = path.resolve(sourceCodePath, "functions"); + const npmInstall = spawn.sync("npm", ["install"], { + encoding: "utf8", + cwd: functionsDirectory, + }); + if (npmInstall.error) { + throw npmInstall.error; + } + this.logger.logLabeled("DEBUG", "Extensions", `Finished "npm install" for ${sourceCodePath}`); + + this.logger.logLabeled( + "DEBUG", + "Extensions", + `Running "npm run gcp-build" for ${sourceCodePath}`, + ); + const npmRunGCPBuild = spawn.sync("npm", ["run", "gcp-build"], { + encoding: "utf8", + cwd: functionsDirectory, + }); + if (npmRunGCPBuild.error) { + // TODO: Make sure this does not error out if "gcp-build" is not defined, but does error if it fails otherwise. + throw npmRunGCPBuild.error; + } + + this.logger.logLabeled( + "DEBUG", + "Extensions", + `Finished "npm run gcp-build" for ${sourceCodePath}`, + ); + } + + /** + * getEmulatableBackends reads firebase.json & .env files for a list of extension instances to emulate, + * downloads & builds the necessary source code (if it hasn't previously been cached), + * then builds returns a list of emulatableBackends + * @return A list of emulatableBackends, one for each extension instance to be emulated + */ + public async getExtensionBackends(): Promise { + await this.readManifest(); + await this.checkAndWarnAPIs(this.want); + this.backends = await Promise.all( + this.want.map((i: planner.DeploymentInstanceSpec) => { + return this.toEmulatableBackend(i); + }), + ); + return this.backends; + } + + /** + * toEmulatableBackend turns a InstanceSpec into an EmulatableBackend which can be run by the Functions emulator. + * It is exported for testing. + */ + public async toEmulatableBackend( + instance: planner.DeploymentInstanceSpec, + ): Promise { + const extensionDir = await this.ensureSourceCode(instance); + + // TODO: This should find package.json, then use that as functionsDir. + const functionsDir = path.join(extensionDir, "functions"); + // TODO(b/213335255): For local extensions, this should include extensionSpec instead of extensionVersion + const env = Object.assign(this.autoPopulatedParams(instance), instance.params); + + const { extensionTriggers, runtime, nonSecretEnv, secretEnvVariables } = + await getExtensionFunctionInfo(instance, env); + const emulatableBackend: EmulatableBackend = { + functionsDir, + runtime, + bin: process.execPath, + env: nonSecretEnv, + codebase: instance.instanceId, // Give each extension its own codebase name so that they don't share workerPools. + secretEnv: secretEnvVariables, + predefinedTriggers: extensionTriggers, + extensionInstanceId: instance.instanceId, + }; + if (instance.ref) { + emulatableBackend.extension = await planner.getExtension(instance); + emulatableBackend.extensionVersion = await planner.getExtensionVersion(instance); + } else if (instance.localPath) { + emulatableBackend.extensionSpec = await planner.getExtensionSpec(instance); + } + + return emulatableBackend; + } + + private autoPopulatedParams(instance: planner.DeploymentInstanceSpec): Record { + const projectId = this.args.projectId; + return { + PROJECT_ID: projectId ?? "", // TODO: Should this fallback to a default? + EXT_INSTANCE_ID: instance.instanceId, + DATABASE_INSTANCE: projectId ?? "", + DATABASE_URL: `https://${projectId}.firebaseio.com`, + STORAGE_BUCKET: `${projectId}.appspot.com`, + ALLOWED_EVENT_TYPES: instance.allowedEventTypes ? instance.allowedEventTypes.join(",") : "", + EVENTARC_CHANNEL: instance.eventarcChannel ?? "", + EVENTARC_CLOUD_EVENT_SOURCE: `projects/${projectId}/instances/${instance.instanceId}`, + }; + } + + private async checkAndWarnAPIs(instances: planner.InstanceSpec[]): Promise { + const apisToWarn = await getUnemulatedAPIs(this.args.projectId, instances); + if (apisToWarn.length) { + const table = new Table({ + head: [ + "API Name", + "Instances using this API", + `Enabled on ${this.args.projectId}`, + `Enable this API`, + ], + style: { head: ["yellow"] }, + }); + for (const apiToWarn of apisToWarn) { + // We use a shortened link here instead of a alias because cli-table behaves poorly with aliased links + const enablementUri = await shortenUrl( + enableApiURI(this.args.projectId, apiToWarn.apiName), + ); + table.push([ + apiToWarn.apiName, + apiToWarn.instanceIds, + apiToWarn.enabled ? "Yes" : "No", + apiToWarn.enabled ? "" : clc.bold(clc.underline(enablementUri)), + ]); + } + if (Constants.isDemoProject(this.args.projectId)) { + this.logger.logLabeled( + "WARN", + "Extensions", + "The following Extensions make calls to Google Cloud APIs that do not have Emulators. " + + `${clc.bold( + this.args.projectId, + )} is a demo project, so these Extensions may not work as expected.\n` + + table.toString(), + ); + } else { + this.logger.logLabeled( + "WARN", + "Extensions", + "The following Extensions make calls to Google Cloud APIs that do not have Emulators. " + + `These calls will go to production Google Cloud APIs which may have real effects on ${clc.bold( + this.args.projectId, + )}.\n` + + table.toString(), + ); + } + } + } + + /** + * Filters out Extension backends that include any unemulated triggers. + * @param backends a list of backends to filter + * @return a list of backends that include only emulated triggers. + */ + public filterUnemulatedTriggers( + options: Options, + backends: EmulatableBackend[], + ): EmulatableBackend[] { + let foundUnemulatedTrigger = false; + const filteredBackends = backends.filter((backend) => { + const unemulatedServices = checkForUnemulatedTriggerTypes(backend, options); + if (unemulatedServices.length) { + foundUnemulatedTrigger = true; + const msg = ` ignored becuase it includes ${unemulatedServices.join( + ", ", + )} triggered functions, and the ${unemulatedServices.join( + ", ", + )} emulator does not exist or is not running.`; + this.logger.logLabeled("WARN", `extensions[${backend.extensionInstanceId}]`, msg); + } + return unemulatedServices.length === 0; + }); + if (foundUnemulatedTrigger) { + const msg = + "No Cloud Functions for these instances will be emulated, because partially emulating an Extension can lead to unexpected behavior. "; + // TODO(joehanley): "To partially emulate these Extension instance anyway, rerun this command with --force"; + this.logger.log("WARN", msg); + } + return filteredBackends; + } + + private extensionDetailsUILink(backend: EmulatableBackend): string { + if (!EmulatorRegistry.isRunning(Emulators.UI) || !backend.extensionInstanceId) { + // If the Emulator UI is not running, or if this is not an Extension backend, return an empty string + return ""; + } + const uiUrl = EmulatorRegistry.url(Emulators.UI); + uiUrl.pathname = `/${Emulators.EXTENSIONS}/${backend.extensionInstanceId}`; + return clc.underline(clc.bold(uiUrl.toString())); + } + + public extensionsInfoTable(options: Options): string { + const filtedBackends = this.filterUnemulatedTriggers(options, this.backends); + const uiRunning = EmulatorRegistry.isRunning(Emulators.UI); + const tableHead = ["Extension Instance Name", "Extension Ref"]; + if (uiRunning) { + tableHead.push("View in Emulator UI"); + } + const table = new Table({ head: tableHead, style: { head: ["yellow"] } }); + for (const b of filtedBackends) { + if (b.extensionInstanceId) { + const tableEntry = [b.extensionInstanceId, b.extensionVersion?.ref || "Local Extension"]; + if (uiRunning) tableEntry.push(this.extensionDetailsUILink(b)); + table.push(tableEntry); + } + } + return table.toString(); + } +} diff --git a/src/emulator/firestoreEmulator.ts b/src/emulator/firestoreEmulator.ts index 6309b94f443..1254c3f5519 100644 --- a/src/emulator/firestoreEmulator.ts +++ b/src/emulator/firestoreEmulator.ts @@ -1,9 +1,8 @@ import * as chokidar from "chokidar"; import * as fs from "fs"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as path from "path"; -import * as api from "../api"; import * as utils from "../utils"; import * as downloadableEmulators from "./downloadableEmulators"; import { EmulatorInfo, EmulatorInstance, Emulators, Severity } from "../emulator/types"; @@ -14,30 +13,38 @@ import { Issue } from "./types"; export interface FirestoreEmulatorArgs { port?: number; host?: string; - projectId?: string; + websocket_port?: number; + project_id?: string; rules?: string; functions_emulator?: string; auto_download?: boolean; seed_from_export?: string; + single_project_mode?: boolean; + single_project_mode_error?: boolean; } -export class FirestoreEmulator implements EmulatorInstance { - static FIRESTORE_EMULATOR_ENV_ALT = "FIREBASE_FIRESTORE_EMULATOR_ADDRESS"; +export interface FirestoreEmulatorInfo extends EmulatorInfo { + // Used for the Emulator UI to connect to the WebSocket server. + // The casing of the fields below is sensitive and important. + // https://github.com/firebase/firebase-tools-ui/blob/2de1e80cce28454da3afeeb373fbbb45a67cb5ef/src/store/config/types.ts#L26-L27 + webSocketHost?: string; + webSocketPort?: number; +} +export class FirestoreEmulator implements EmulatorInstance { rulesWatcher?: chokidar.FSWatcher; constructor(private args: FirestoreEmulatorArgs) {} async start(): Promise { - const functionsInfo = EmulatorRegistry.getInfo(Emulators.FUNCTIONS); - if (functionsInfo) { - this.args.functions_emulator = EmulatorRegistry.getInfoHostString(functionsInfo); + if (EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { + this.args.functions_emulator = EmulatorRegistry.url(Emulators.FUNCTIONS).host; } - if (this.args.rules && this.args.projectId) { + if (this.args.rules && this.args.project_id) { const rulesPath = this.args.rules; this.rulesWatcher = chokidar.watch(rulesPath, { persistent: true, ignoreInitial: true }); - this.rulesWatcher.on("change", async (event, stats) => { + this.rulesWatcher.on("change", async () => { // There have been some race conditions reported (on Windows) where reading the // file too quickly after the watcher fires results in an empty file being read. // Adding a small delay prevents that at very little cost. @@ -74,15 +81,19 @@ export class FirestoreEmulator implements EmulatorInstance { return downloadableEmulators.stop(Emulators.FIRESTORE); } - getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.FIRESTORE); + getInfo(): FirestoreEmulatorInfo { + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.FIRESTORE); + const reservedPorts = this.args.websocket_port ? [this.args.websocket_port] : []; return { name: this.getName(), host, port, pid: downloadableEmulators.getPID(Emulators.FIRESTORE), + reservedPorts: reservedPorts, + webSocketHost: this.args.websocket_port ? host : undefined, + webSocketPort: this.args.websocket_port ? this.args.websocket_port : undefined, }; } @@ -90,10 +101,9 @@ export class FirestoreEmulator implements EmulatorInstance { return Emulators.FIRESTORE; } - private updateRules(content: string): Promise { - const projectId = this.args.projectId; + private async updateRules(content: string): Promise { + const projectId = this.args.project_id; - const info = this.getInfo(); const body = { // Invalid rulesets will still result in a 200 response but with more information ignore_errors: true, @@ -107,18 +117,14 @@ export class FirestoreEmulator implements EmulatorInstance { }, }; - return api - .request("PUT", `/emulator/v1/projects/${projectId}:securityRules`, { - origin: `http://${EmulatorRegistry.getInfoHostString(info)}`, - data: body, - }) - .then((res) => { - if (res.body && res.body.issues) { - return res.body.issues as Issue[]; - } - - return []; - }); + const res = await EmulatorRegistry.client(Emulators.FIRESTORE).put( + `/emulator/v1/projects/${projectId}:securityRules`, + body, + ); + if (res.body && Array.isArray(res.body.issues)) { + return res.body.issues; + } + return []; } /** @@ -130,7 +136,7 @@ export class FirestoreEmulator implements EmulatorInstance { const line = issue.sourcePosition.line || 0; const col = issue.sourcePosition.column || 0; return `${clc.cyan(relativePath)}:${clc.yellow(line)}:${clc.yellow(col)} - ${clc.red( - issue.severity + issue.severity, )} ${issue.description}`; } } diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 4d7ca8cb502..b29edf669cb 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -1,63 +1,70 @@ -import * as _ from "lodash"; import * as fs from "fs"; import * as path from "path"; import * as express from "express"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as http from "http"; import * as jwt from "jsonwebtoken"; +import * as cors from "cors"; +import * as semver from "semver"; import { URL } from "url"; +import { EventEmitter } from "events"; -import { Account } from "../auth"; -import * as api from "../api"; +import { Account } from "../types/auth"; import { logger } from "../logger"; -import * as track from "../track"; +import { trackEmulator } from "../track"; import { Constants } from "./constants"; -import { - EmulatorInfo, - EmulatorInstance, - EmulatorLog, - Emulators, - FunctionsExecutionMode, -} from "./types"; +import { EmulatorInfo, EmulatorInstance, Emulators, FunctionsExecutionMode } from "./types"; import * as chokidar from "chokidar"; +import * as portfinder from "portfinder"; import * as spawn from "cross-spawn"; -import { ChildProcess, spawnSync } from "child_process"; +import { ChildProcess } from "child_process"; import { - emulatedFunctionsByRegion, EmulatedTriggerDefinition, SignatureType, EventSchedule, EventTrigger, formatHost, - FunctionsRuntimeArgs, - FunctionsRuntimeBundle, FunctionsRuntimeFeatures, getFunctionService, getSignatureType, HttpConstants, ParsedTriggerDefinition, + emulatedFunctionsFromEndpoints, + emulatedFunctionsByRegion, + getSecretLocalPath, + toBackendInfo, + prepareEndpoints, + BlockingTrigger, + getTemporarySocketPath, } from "./functionsEmulatorShared"; import { EmulatorRegistry } from "./registry"; -import { EventEmitter } from "events"; -import * as stream from "stream"; import { EmulatorLogger, Verbosity } from "./emulatorLogger"; import { RuntimeWorker, RuntimeWorkerPool } from "./functionsRuntimeWorker"; import { PubsubEmulator } from "./pubsubEmulator"; import { FirebaseError } from "../error"; -import { WorkQueue } from "./workQueue"; -import { createDestroyer } from "../utils"; +import { WorkQueue, Work } from "./workQueue"; +import { allSettled, connectableHostname, createDestroyer, debounce, randomInt } from "../utils"; import { getCredentialPathAsync } from "../defaultCredentials"; import { AdminSdkConfig, constructDefaultAdminSdkConfig, getProjectAdminSdkConfigOrCached, } from "./adminSdkConfig"; -import * as functionsEnv from "../functions/env"; -import { EventUtils } from "./events/types"; import { functionIdsAreValid } from "../deploy/functions/validate"; +import { Extension, ExtensionSpec, ExtensionVersion } from "../extensions/types"; +import { accessSecretVersion } from "../gcp/secretManager"; +import * as runtimes from "../deploy/functions/runtimes"; +import * as backend from "../deploy/functions/backend"; +import * as functionsEnv from "../functions/env"; +import { AUTH_BLOCKING_EVENTS, BEFORE_CREATE_EVENT } from "../functions/events/v1"; +import { BlockingFunctionsConfig } from "../gcp/identityPlatform"; +import { resolveBackend } from "../deploy/functions/build"; +import { setEnvVarsForEmulators } from "./env"; +import { runWithVirtualEnv } from "../functions/python"; +import { Runtime } from "../deploy/functions/runtimes/supported"; -const EVENT_INVOKE = "functions:invoke"; +const EVENT_INVOKE_GA4 = "functions_invoke"; // event name GA4 (alphanumertic) /* * The Realtime Database emulator expects the `path` field in its trigger @@ -71,41 +78,94 @@ const EVENT_INVOKE = "functions:invoke"; */ const DATABASE_PATH_PATTERN = new RegExp("^projects/[^/]+/instances/([^/]+)/refs(/.*)$"); +/** + * EmulatableBackend represents a group of functions to be emulated. + * This can be a CF3 module, or an Extension. + */ +export interface EmulatableBackend { + functionsDir: string; + env: Record; + secretEnv: backend.SecretEnvVar[]; + codebase: string; + predefinedTriggers?: ParsedTriggerDefinition[]; + runtime?: Runtime; + bin?: string; + extensionInstanceId?: string; + extension?: Extension; // Only present for published extensions + extensionVersion?: ExtensionVersion; // Only present for published extensions + extensionSpec?: ExtensionSpec; // Only present for local extensions +} + +/** + * BackendInfo is an API type used by the Emulator UI containing info about an Extension or CF3 module. + */ +export interface BackendInfo { + directory: string; + env: Record; // TODO: Consider exposing more information about where param values come from & if they are locally overwritten. + functionTriggers: ParsedTriggerDefinition[]; + extensionInstanceId?: string; + extension?: Extension; // Only present for published extensions + extensionVersion?: ExtensionVersion; // Only present for published extensions + extensionSpec?: ExtensionSpec; // Only present for local extensions +} + export interface FunctionsEmulatorArgs { projectId: string; - functionsDir: string; + projectDir: string; + emulatableBackends: EmulatableBackend[]; + debugPort: number | boolean; account?: Account; port?: number; host?: string; - quiet?: boolean; + verbosity?: "SILENT" | "QUIET" | "INFO" | "DEBUG"; disabledRuntimeFeatures?: FunctionsRuntimeFeatures; - debugPort?: number; - env?: Record; - remoteEmulators?: { [key: string]: EmulatorInfo }; - predefinedTriggers?: ParsedTriggerDefinition[]; - nodeMajorVersion?: number; // Lets us specify the node version when emulating extensions. + remoteEmulators?: Record; + adminSdkConfig?: AdminSdkConfig; + projectAlias?: string; +} + +/** + * IPC connection info of a Function Runtime. + */ +export class IPCConn { + constructor(readonly socketPath: string) {} + + httpReqOpts(): http.RequestOptions { + return { + socketPath: this.socketPath, + }; + } +} + +/** + * TCP/IP connection info of a Function Runtime. + */ +export class TCPConn { + constructor( + readonly host: string, + readonly port: number, + ) {} + + httpReqOpts(): http.RequestOptions { + return { + host: this.host, + port: this.port, + }; + } } -// FunctionsRuntimeInstance is the handler for a running function invocation export interface FunctionsRuntimeInstance { - // Process ID - pid: number; + process: ChildProcess; // An emitter which sends our EmulatorLog events from the runtime. events: EventEmitter; - // A promise which is fulfilled when the runtime has exited - exit: Promise; - - // A function to manually kill the child process as normal cleanup - shutdown(): void; - // A function to manually kill the child process in case of errors - kill(signal?: string): void; - // Send an IPC message to the child process - send(args: FunctionsRuntimeArgs): boolean; + // A cwd of the process + cwd: string; + // Communication info for the runtime + conn: IPCConn | TCPConn; } export interface InvokeRuntimeOpts { nodeBinary: string; - serializedTriggers?: string; extensionTriggers?: ParsedTriggerDefinition[]; ignore_warnings?: boolean; } @@ -115,55 +175,83 @@ interface RequestWithRawBody extends express.Request { } interface EmulatedTriggerRecord { + backend: EmulatableBackend; def: EmulatedTriggerDefinition; enabled: boolean; ignored: boolean; + url?: string; } export class FunctionsEmulator implements EmulatorInstance { static getHttpFunctionUrl( - host: string, - port: number, projectId: string, name: string, - region: string + region: string, + info?: { host: string; port: number }, ): string { - return `http://${host}:${port}/${projectId}/${region}/${name}`; + let url: URL; + if (info) { + url = new URL("http://" + formatHost(info)); + } else { + url = EmulatorRegistry.url(Emulators.FUNCTIONS); + } + url.pathname = `/${projectId}/${region}/${name}`; + return url.toString(); } - nodeBinary = ""; private destroyServer?: () => Promise; private triggers: { [triggerName: string]: EmulatedTriggerRecord } = {}; // Keep a "generation number" for triggers so that we can disable functions // and reload them with a new name. private triggerGeneration = 0; - private workerPool: RuntimeWorkerPool; + private workerPools: Record; private workQueue: WorkQueue; private logger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); private multicastTriggers: { [s: string]: string[] } = {}; private adminSdkConfig: AdminSdkConfig; + private blockingFunctionsConfig: BlockingFunctionsConfig = {}; + + debugMode = false; + constructor(private args: FunctionsEmulatorArgs) { // TODO: Would prefer not to have static state but here we are! - EmulatorLogger.verbosity = this.args.quiet ? Verbosity.QUIET : Verbosity.DEBUG; + EmulatorLogger.setVerbosity( + this.args.verbosity ? Verbosity[this.args.verbosity] : Verbosity["DEBUG"], + ); // When debugging is enabled, the "timeout" feature needs to be disabled so that // functions don't timeout while a breakpoint is active. if (this.args.debugPort) { + // N.B. Technically this will create false positives where there is a Node + // and a Python codebase, but there is no good place to check the runtime + // because that may not be present until discovery (e.g. node codebases + // return their runtime based on package.json if not specified in + // firebase.json) + const maybeNodeCodebases = this.args.emulatableBackends.filter( + (b) => !b.runtime || b.runtime.startsWith("node"), + ); + if (maybeNodeCodebases.length > 1 && typeof this.args.debugPort === "number") { + throw new FirebaseError( + "Cannot debug on a single port with multiple codebases. " + + "Use --inspect-functions=true to assign dynamic ports to each codebase", + ); + } this.args.disabledRuntimeFeatures = this.args.disabledRuntimeFeatures || {}; this.args.disabledRuntimeFeatures.timeout = true; + this.debugMode = true; } - this.adminSdkConfig = { - projectId: this.args.projectId, - }; + this.adminSdkConfig = { ...this.args.adminSdkConfig, projectId: this.args.projectId }; - const mode = this.args.debugPort - ? FunctionsExecutionMode.SEQUENTIAL - : FunctionsExecutionMode.AUTO; - this.workerPool = new RuntimeWorkerPool(mode); + const mode = this.debugMode ? FunctionsExecutionMode.SEQUENTIAL : FunctionsExecutionMode.AUTO; + this.workerPools = {}; + for (const backend of this.args.emulatableBackends) { + const pool = new RuntimeWorkerPool(mode); + this.workerPools[backend.codebase] = pool; + } this.workQueue = new WorkQueue(mode); } @@ -174,7 +262,7 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.logLabeled( "WARN", "functions", - `Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!` + `Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!`, ); } else if (this.args.account) { const defaultCredPath = await getCredentialPathAsync(this.args.account); @@ -188,7 +276,7 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.logLabeled( "WARN", "functions", - "You are not signed in to the Firebase CLI. If you have authorized this machine using gcloud application-default credentials those may be discovered and used to access production services." + "You are not signed in to the Firebase CLI. If you have authorized this machine using gcloud application-default credentials those may be discovered and used to access production services.", ); } @@ -197,7 +285,7 @@ export class FunctionsEmulator implements EmulatorInstance { createHubServer(): express.Application { // TODO(samstern): Should not need this here but some tests are directly calling this method - // because FunctionsEmulator.start() is not test-safe due to askInstallNodeVersion. + // because FunctionsEmulator.start() used to not be test safe. this.workQueue.start(); const hub = express(); @@ -215,7 +303,7 @@ export class FunctionsEmulator implements EmulatorInstance { // The URL for the function that the other emulators (Firestore, etc) use. // TODO(abehaskins): Make the other emulators use the route below and remove this. - const backgroundFunctionRoute = `/functions/projects/:project_id/triggers/:trigger_name`; + const backgroundFunctionRoute = `/functions/projects/:project_id/triggers/:trigger_name(*)`; // The URL that the developer sees, this is the same URL that the legacy emulator used. const httpsFunctionRoute = `/${this.args.projectId}/:region/:trigger_name`; @@ -226,85 +314,64 @@ export class FunctionsEmulator implements EmulatorInstance { // A trigger named "foo" needs to respond at "foo" as well as "foo/*" but not "fooBar". const httpsFunctionRoutes = [httpsFunctionRoute, `${httpsFunctionRoute}/*`]; - const backgroundHandler: express.RequestHandler = (req, res) => { - const region = req.params.region; - const triggerId = req.params.trigger_name; - const projectId = req.params.project_id; - - const reqBody = (req as RequestWithRawBody).rawBody; - let proto = JSON.parse(reqBody.toString()); - - if (req.headers["content-type"]?.includes("cloudevent")) { - // Convert request payload to CloudEvent. - // TODO(taeold): Converting request payload to CloudEvent object should be done by the functions runtime. - // However, the Functions Emulator communicates with the runtime via socket not HTTP, and CE metadata - // embedded in HTTP header may get lost. Once the Functions Emulator is refactored to communicate to the - // runtime instances via HTTP, move this logic there. - if (EventUtils.isBinaryCloudEvent(req)) { - proto = EventUtils.extractBinaryCloudEventContext(req); - proto.data = req.body; - } - } - - this.workQueue.submit(() => { - this.logger.log("DEBUG", `Accepted request ${req.method} ${req.url} --> ${triggerId}`); - - return this.handleBackgroundTrigger(projectId, triggerId, proto) - .then((x) => res.json(x)) - .catch((errorBundle: { code: number; body?: string }) => { - if (errorBundle.body) { - res.status(errorBundle.code).send(errorBundle.body); - } else { - res.sendStatus(errorBundle.code); - } - }); - }); - }; + // The URL for the listBackends endpoint, which is used by the Emulator UI. + const listBackendsRoute = `/backends`; const httpsHandler: express.RequestHandler = (req, res) => { - this.workQueue.submit(() => { + const work: Work = () => { return this.handleHttpsTrigger(req, res); - }); + }; + work.type = `${req.path}-${new Date().toISOString()}`; + this.workQueue.submit(work); }; - const multicastHandler: express.RequestHandler = (req, res) => { + const multicastHandler: express.RequestHandler = (req: express.Request, res) => { const projectId = req.params.project_id; - const reqBody = (req as RequestWithRawBody).rawBody; - let proto = JSON.parse(reqBody.toString()); + const rawBody = (req as RequestWithRawBody).rawBody; + const event = JSON.parse(rawBody.toString()); let triggerKey: string; if (req.headers["content-type"]?.includes("cloudevent")) { - triggerKey = `${this.args.projectId}:${proto.type}`; - - if (EventUtils.isBinaryCloudEvent(req)) { - proto = EventUtils.extractBinaryCloudEventContext(req); - proto.data = req.body; - } + triggerKey = `${this.args.projectId}:${event.type}`; } else { - triggerKey = `${this.args.projectId}:${proto.eventType}`; + triggerKey = `${this.args.projectId}:${event.eventType}`; } - if (proto.data.bucket) { - triggerKey += `:${proto.data.bucket}`; + if (event.data.bucket) { + triggerKey += `:${event.data.bucket}`; } const triggers = this.multicastTriggers[triggerKey] || []; + const { host, port } = this.getInfo(); triggers.forEach((triggerId) => { - this.workQueue.submit(() => { - this.logger.log( - "DEBUG", - `Accepted multicast request ${req.method} ${req.url} --> ${triggerId}` - ); - - return this.handleBackgroundTrigger(projectId, triggerId, proto); - }); + const work: Work = () => { + return new Promise((resolve, reject) => { + const trigReq = http.request({ + host: connectableHostname(host), + port, + method: req.method, + path: `/functions/projects/${projectId}/triggers/${triggerId}`, + headers: req.headers, + }); + trigReq.on("error", reject); + trigReq.write(rawBody); + trigReq.end(); + resolve(); + }); + }; + work.type = `${triggerId}-${new Date().toISOString()}`; + this.workQueue.submit(work); }); - res.json({ status: "multicast_acknowledged" }); }; + const listBackendsHandler: express.RequestHandler = (req, res) => { + res.json({ backends: this.getBackendInfo() }); + }; + // The ordering here is important. The longer routes (background) // need to be registered first otherwise the HTTP functions consume // all events. - hub.post(backgroundFunctionRoute, dataMiddleware, backgroundHandler); + hub.get(listBackendsRoute, cors({ origin: true }), listBackendsHandler); // This route needs CORS so the Emulator UI can call it. + hub.post(backgroundFunctionRoute, dataMiddleware, httpsHandler); hub.post(multicastFunctionRoute, dataMiddleware, multicastHandler); hub.all(httpsFunctionRoutes, dataMiddleware, httpsHandler); hub.all("*", dataMiddleware, (req, res) => { @@ -314,62 +381,62 @@ export class FunctionsEmulator implements EmulatorInstance { return hub; } - startFunctionRuntime( - triggerId: string, - targetName: string, - signatureType: SignatureType, - proto?: any, - runtimeOpts?: InvokeRuntimeOpts - ): RuntimeWorker { - const bundleTemplate = this.getBaseBundle(); - const runtimeBundle: FunctionsRuntimeBundle = { - ...bundleTemplate, - emulators: { - firestore: this.getEmulatorInfo(Emulators.FIRESTORE), - database: this.getEmulatorInfo(Emulators.DATABASE), - pubsub: this.getEmulatorInfo(Emulators.PUBSUB), - auth: this.getEmulatorInfo(Emulators.AUTH), - storage: this.getEmulatorInfo(Emulators.STORAGE), - }, - nodeMajorVersion: this.args.nodeMajorVersion, - proto, - triggerId, - targetName, - }; - const opts = runtimeOpts || { - nodeBinary: this.nodeBinary, - extensionTriggers: this.args.predefinedTriggers, + async sendRequest(trigger: EmulatedTriggerDefinition, body?: any) { + const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger)); + const pool = this.workerPools[record.backend.codebase]; + if (!pool.readyForWork(trigger.id)) { + try { + await this.startRuntime(record.backend, trigger); + } catch (e: any) { + this.logger.logLabeled("ERROR", `Failed to start runtime for ${trigger.id}: ${e}`); + return; + } + } + const worker = pool.getIdleWorker(trigger.id)!; + if (this.debugMode) { + await worker.sendDebugMsg({ + functionTarget: trigger.entryPoint, + functionSignature: getSignatureType(trigger), + }); + } + const reqBody = JSON.stringify(body); + const headers = { + "Content-Type": "application/json", + "Content-Length": `${reqBody.length}`, }; - const worker = this.invokeRuntime( - runtimeBundle, - opts, - this.getRuntimeEnvs({ targetName, signatureType }) - ); - return worker; + return new Promise((resolve, reject) => { + const req = http.request( + { + ...worker.runtime.conn.httpReqOpts(), + path: `/`, + headers: headers, + }, + resolve, + ); + req.on("error", reject); + req.write(reqBody); + req.end(); + }); } async start(): Promise { - this.nodeBinary = this.askInstallNodeVersion( - this.args.functionsDir, - this.args.nodeMajorVersion - ); - const credentialEnv = await this.getCredentialsEnvironment(); - this.args.env = { - ...credentialEnv, - ...this.args.env, - }; + for (const e of this.args.emulatableBackends) { + e.env = { ...credentialEnv, ...e.env }; + } - const adminSdkConfig = await getProjectAdminSdkConfigOrCached(this.args.projectId); - if (adminSdkConfig) { - this.adminSdkConfig = adminSdkConfig; - } else { - this.logger.logLabeled( - "WARN", - "functions", - "Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect." - ); - this.adminSdkConfig = constructDefaultAdminSdkConfig(this.args.projectId); + if (Object.keys(this.adminSdkConfig || {}).length <= 1) { + const adminSdkConfig = await getProjectAdminSdkConfigOrCached(this.args.projectId); + if (adminSdkConfig) { + this.adminSdkConfig = adminSdkConfig; + } else { + this.logger.logLabeled( + "WARN", + "functions", + "Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.", + ); + this.adminSdkConfig = constructDefaultAdminSdkConfig(this.args.projectId); + } } const { host, port } = this.getInfo(); @@ -380,91 +447,145 @@ export class FunctionsEmulator implements EmulatorInstance { } async connect(): Promise { - this.logger.logLabeled( - "BULLET", - "functions", - `Watching "${this.args.functionsDir}" for Cloud Functions...` - ); + for (const backend of this.args.emulatableBackends) { + this.logger.logLabeled( + "BULLET", + "functions", + `Watching "${backend.functionsDir}" for Cloud Functions...`, + ); - const watcher = chokidar.watch(this.args.functionsDir, { - ignored: [ - /.+?[\\\/]node_modules[\\\/].+?/, // Ignore node_modules - /(^|[\/\\])\../, // Ignore files which begin the a period - /.+\.log/, // Ignore files which have a .log extension - ], - persistent: true, - }); + const watcher = chokidar.watch(backend.functionsDir, { + ignored: [ + /.+?[\\\/]node_modules[\\\/].+?/, // Ignore node_modules + /(^|[\/\\])\../, // Ignore files which begin the a period + /.+\.log/, // Ignore files which have a .log extension + /.+?[\\\/]venv[\\\/].+?/, // Ignore site-packages in venv + ], + persistent: true, + }); - const debouncedLoadTriggers = _.debounce(() => this.loadTriggers(), 1000); - watcher.on("change", (filePath) => { - this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`); - return debouncedLoadTriggers(); - }); + const debouncedLoadTriggers = debounce(() => this.loadTriggers(backend), 1000); + watcher.on("change", (filePath) => { + this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`); + return debouncedLoadTriggers(); + }); - return this.loadTriggers(/* force= */ true); + await this.loadTriggers(backend, /* force= */ true); + } + await this.performPostLoadOperations(); + return; } async stop(): Promise { try { await this.workQueue.flush(); - } catch (e) { + } catch (e: any) { this.logger.logLabeled( "WARN", "functions", - "Functions emulator work queue did not empty before stopping" + "Functions emulator work queue did not empty before stopping", ); } this.workQueue.stop(); - this.workerPool.exit(); + for (const pool of Object.values(this.workerPools)) { + pool.exit(); + } if (this.destroyServer) { await this.destroyServer(); } } + async discoverTriggers( + emulatableBackend: EmulatableBackend, + ): Promise { + if (emulatableBackend.predefinedTriggers) { + return emulatedFunctionsByRegion( + emulatableBackend.predefinedTriggers, + emulatableBackend.secretEnv, + ); + } else { + const runtimeConfig = this.getRuntimeConfig(emulatableBackend); + const runtimeDelegateContext: runtimes.DelegateContext = { + projectId: this.args.projectId, + projectDir: this.args.projectDir, + sourceDir: emulatableBackend.functionsDir, + runtime: emulatableBackend.runtime, + }; + const runtimeDelegate = await runtimes.getRuntimeDelegate(runtimeDelegateContext); + logger.debug(`Validating ${runtimeDelegate.language} source`); + await runtimeDelegate.validate(); + logger.debug(`Building ${runtimeDelegate.language} source`); + await runtimeDelegate.build(); + + // Retrieve information from the runtime delegate. + emulatableBackend.runtime = runtimeDelegate.runtime; + emulatableBackend.bin = runtimeDelegate.bin; + + // Don't include user envs when parsing triggers. Do include user envs when resolving parameter values + const firebaseConfig = this.getFirebaseConfig(); + const environment = { + ...this.getSystemEnvs(), + ...this.getEmulatorEnvs(), + FIREBASE_CONFIG: firebaseConfig, + ...emulatableBackend.env, + }; + const userEnvOpt: functionsEnv.UserEnvsOpts = { + functionsSource: emulatableBackend.functionsDir, + projectId: this.args.projectId, + projectAlias: this.args.projectAlias, + isEmulator: true, + }; + const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt); + const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment); + const resolution = await resolveBackend( + discoveredBuild, + JSON.parse(firebaseConfig), + userEnvOpt, + userEnvs, + ); + const discoveredBackend = resolution.backend; + const endpoints = backend.allEndpoints(discoveredBackend); + prepareEndpoints(endpoints); + for (const e of endpoints) { + e.codebase = emulatableBackend.codebase; + } + return emulatedFunctionsFromEndpoints(endpoints); + } + } + /** * When a user changes their code, we need to look for triggers defined in their updates sources. - * To do this, we spin up a "diagnostic" runtime invocation. In other words, we pretend we're - * going to invoke a cloud function in the emulator, but stop short of actually running a function. - * Instead, we set up the environment and catch a special "triggers-parsed" log from the runtime - * then exit out. - * - * A "diagnostic" FunctionsRuntimeBundle looks just like a normal bundle except triggerId == "". * - * TODO(abehaskins): Gracefully handle removal of deleted function definitions + * TODO(b/216167890): Gracefully handle removal of deleted function definitions */ - async loadTriggers(force = false): Promise { + async loadTriggers(emulatableBackend: EmulatableBackend, force = false): Promise { + let triggerDefinitions: EmulatedTriggerDefinition[] = []; + try { + triggerDefinitions = await this.discoverTriggers(emulatableBackend); + this.logger.logLabeled( + "SUCCESS", + "functions", + `Loaded functions definitions from source: ${triggerDefinitions + .map((t) => t.entryPoint) + .join(", ")}.`, + ); + } catch (e) { + this.logger.logLabeled( + "ERROR", + "functions", + `Failed to load function definition from source: ${e}`, + ); + return; + } // Before loading any triggers we need to make sure there are no 'stale' workers // in the pool that would cause us to run old code. - this.workerPool.refresh(); - - const worker = this.invokeRuntime( - this.getBaseBundle(), - { - nodeBinary: this.nodeBinary, - extensionTriggers: this.args.predefinedTriggers, - }, - // Don't include user envs when parsing triggers. - { - ...this.getSystemEnvs(), - ...this.getEmulatorEnvs(), - FIREBASE_CONFIG: this.getFirebaseConfig(), - ...this.args.env, - } - ); - - const triggerParseEvent = await EmulatorLog.waitForLog( - worker.runtime.events, - "SYSTEM", - "triggers-parsed" - ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const parsedDefinitions = triggerParseEvent.data - .triggerDefinitions as ParsedTriggerDefinition[]; - - const triggerDefinitions: EmulatedTriggerDefinition[] = emulatedFunctionsByRegion( - parsedDefinitions - ); + if (this.debugMode) { + // Kill the workerPool. This should clean up all inspectors connected to the debug port. + this.workerPools[emulatableBackend.codebase].exit(); + } else { + this.workerPools[emulatableBackend.codebase].refresh(); + } // When force is true we set up all triggers, otherwise we only set up // triggers which have a unique function name @@ -472,7 +593,6 @@ export class FunctionsEmulator implements EmulatorInstance { if (force) { return true; } - // We want to add a trigger if we don't already have an enabled trigger // with the same entryPoint / trigger. const anyEnabledMatch = Object.values(this.triggers).some((record) => { @@ -486,8 +606,8 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.log( "DEBUG", `Definition for trigger ${definition.entryPoint} changed from ${JSON.stringify( - record.def.eventTrigger - )} to ${JSON.stringify(definition.eventTrigger)}` + record.def.eventTrigger, + )} to ${JSON.stringify(definition.eventTrigger)}`, ); } @@ -499,28 +619,22 @@ export class FunctionsEmulator implements EmulatorInstance { for (const definition of toSetup) { // Skip function with invalid id. try { - functionIdsAreValid([definition]); - } catch (e) { - this.logger.logLabeled( - "WARN", - `functions[${definition.id}]`, - `Invalid function id: ${e.message}` - ); - continue; + // Note - in the emulator, functionId = {region}-{functionName}, but in prod, functionId=functionName. + // To match prod behavior, only validate functionName + functionIdsAreValid([{ ...definition, id: definition.name }]); + } catch (e: any) { + throw new FirebaseError(`functions[${definition.id}]: Invalid function id: ${e.message}`); } let added = false; let url: string | undefined = undefined; if (definition.httpsTrigger) { - const { host, port } = this.getInfo(); added = true; url = FunctionsEmulator.getHttpFunctionUrl( - host, - port, this.args.projectId, definition.name, - definition.region + definition.region, ); } else if (definition.eventTrigger) { const service: string = getFunctionService(definition); @@ -532,14 +646,18 @@ export class FunctionsEmulator implements EmulatorInstance { added = await this.addFirestoreTrigger( this.args.projectId, key, - definition.eventTrigger + definition.eventTrigger, + signature, ); break; case Constants.SERVICE_REALTIME_DATABASE: added = await this.addRealtimeDatabaseTrigger( this.args.projectId, + definition.id, key, - definition.eventTrigger + definition.eventTrigger, + signature, + definition.region, ); break; case Constants.SERVICE_PUBSUB: @@ -548,7 +666,14 @@ export class FunctionsEmulator implements EmulatorInstance { key, definition.eventTrigger, signature, - definition.schedule + definition.schedule, + ); + break; + case Constants.SERVICE_EVENTARC: + added = await this.addEventarcTrigger( + this.args.projectId, + key, + definition.eventTrigger, ); break; case Constants.SERVICE_AUTH: @@ -561,15 +686,22 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.log("DEBUG", `Unsupported trigger: ${JSON.stringify(definition)}`); break; } + } else if (definition.blockingTrigger) { + url = FunctionsEmulator.getHttpFunctionUrl( + this.args.projectId, + definition.name, + definition.region, + ); + added = this.addBlockingTrigger(url, definition.blockingTrigger); } else { this.logger.log( "WARN", - `Trigger trigger "${definition.name}" has has neither "httpsTrigger" or "eventTrigger" member` + `Unsupported function type on ${definition.name}. Expected either an httpsTrigger, eventTrigger, or blockingTrigger.`, ); } const ignored = !added; - this.addTriggerRecord(definition, { ignored, url }); + this.addTriggerRecord(definition, { backend: emulatableBackend, ignored, url }); const type = definition.httpsTrigger ? "http" @@ -585,25 +717,97 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.logLabeled("SUCCESS", `functions[${definition.id}]`, msg); } } + // In debug mode, we eagerly start the runtime processes to allow debuggers to attach + // before invoking a function. + if (this.debugMode) { + if (!emulatableBackend.runtime?.startsWith("node")) { + this.logger.log("WARN", "--inspect-functions only supported for Node.js runtimes."); + } else { + // Since we're about to start a runtime to be shared by all the functions in this codebase, + // we need to make sure it has all the secrets used by any function in the codebase. + emulatableBackend.secretEnv = Object.values( + triggerDefinitions.reduce( + (acc: Record, curr: EmulatedTriggerDefinition) => { + for (const secret of curr.secretEnvironmentVariables || []) { + acc[secret.key] = secret; + } + return acc; + }, + {}, + ), + ); + try { + await this.startRuntime(emulatableBackend); + } catch (e: any) { + this.logger.logLabeled( + "ERROR", + `Failed to start functions in ${emulatableBackend.functionsDir}: ${e}`, + ); + } + } + } } - addRealtimeDatabaseTrigger( - projectId: string, - key: string, - eventTrigger: EventTrigger - ): Promise { - const databaseEmu = EmulatorRegistry.get(Emulators.DATABASE); - if (!databaseEmu) { + addEventarcTrigger(projectId: string, key: string, eventTrigger: EventTrigger): Promise { + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { return Promise.resolve(false); } + const bundle = { + eventTrigger: { + ...eventTrigger, + service: "eventarc.googleapis.com", + }, + }; + logger.debug(`addEventarcTrigger`, JSON.stringify(bundle)); + return EmulatorRegistry.client(Emulators.EVENTARC) + .post(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle) + .then(() => true) + .catch((err) => { + this.logger.log("WARN", "Error adding Eventarc function: " + err); + return false; + }); + } + + async performPostLoadOperations(): Promise { + if ( + !this.blockingFunctionsConfig.triggers && + !this.blockingFunctionsConfig.forwardInboundCredentials + ) { + return; + } + + if (!EmulatorRegistry.isRunning(Emulators.AUTH)) { + return; + } + + const path = `/identitytoolkit.googleapis.com/v2/projects/${this.getProjectId()}/config?updateMask=blockingFunctions`; + + try { + const client = EmulatorRegistry.client(Emulators.AUTH); + await client.patch( + path, + { blockingFunctions: this.blockingFunctionsConfig }, + { + headers: { Authorization: "Bearer owner" }, + }, + ); + } catch (err) { + this.logger.log( + "WARN", + "Error updating blocking functions config to the auth emulator: " + err, + ); + throw err; + } + } - const result: string[] | null = DATABASE_PATH_PATTERN.exec(eventTrigger.resource); + private getV1DatabaseApiAttributes(projectId: string, key: string, eventTrigger: EventTrigger) { + const result: string[] | null = DATABASE_PATH_PATTERN.exec(eventTrigger.resource!); if (result === null || result.length !== 3) { this.logger.log( "WARN", - `Event trigger "${key}" has malformed "resource" member. ` + `${eventTrigger.resource}` + `Event function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource!}`, ); - return Promise.reject(); + throw new FirebaseError(`Event function ${key} has malformed resource member`); } const instance = result[1]; @@ -614,62 +818,163 @@ export class FunctionsEmulator implements EmulatorInstance { topic: `projects/${projectId}/topics/${key}`, }); - logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle)); - - let setTriggersPath = "/.settings/functionTriggers.json"; + let apiPath = "/.settings/functionTriggers.json"; if (instance !== "") { - setTriggersPath += `?ns=${instance}`; + apiPath += `?ns=${instance}`; } else { this.logger.log( "WARN", - `No project in use. Registering function trigger for sentinel namespace '${Constants.DEFAULT_DATABASE_EMULATOR_NAMESPACE}'` + `No project in use. Registering function for sentinel namespace '${Constants.DEFAULT_DATABASE_EMULATOR_NAMESPACE}'`, ); } - return api - .request("POST", setTriggersPath, { - origin: `http://${EmulatorRegistry.getInfoHostString(databaseEmu.getInfo())}`, - headers: { - Authorization: "Bearer owner", - }, - data: bundle, - json: false, - }) - .then(() => { - return true; - }) - .catch((err) => { - this.logger.log("WARN", "Error adding trigger: " + err); - throw err; - }); + return { bundle, apiPath, instance }; + } + + private getV2DatabaseApiAttributes( + projectId: string, + id: string, + key: string, + eventTrigger: EventTrigger, + region: string, + ) { + const instance = + eventTrigger.eventFilters?.instance || eventTrigger.eventFilterPathPatterns?.instance; + if (!instance) { + throw new FirebaseError("A database instance must be supplied."); + } + + const ref = eventTrigger.eventFilterPathPatterns?.ref; + if (!ref) { + throw new FirebaseError("A database reference must be supplied."); + } + + // TODO(colerogers): yank/change if RTDB emulator ever supports multiple regions + if (region !== "us-central1") { + this.logger.logLabeled( + "WARN", + `functions[${id}]`, + `function region is defined outside the database region, will not trigger.`, + ); + } + + // The 'namespacePattern' determines that we are using the v2 interface + const bundle = JSON.stringify({ + name: `projects/${projectId}/locations/${region}/triggers/${key}`, + path: ref, + event: eventTrigger.eventType, + topic: `projects/${projectId}/topics/${key}`, + namespacePattern: instance, + }); + + // The query parameter '?ns=${instance}' is ignored in v2 + const apiPath = "/.settings/functionTriggers.json"; + + return { bundle, apiPath, instance }; + } + + async addRealtimeDatabaseTrigger( + projectId: string, + id: string, + key: string, + eventTrigger: EventTrigger, + signature: SignatureType, + region: string, + ): Promise { + if (!EmulatorRegistry.isRunning(Emulators.DATABASE)) { + return false; + } + + const { bundle, apiPath, instance } = + signature === "cloudevent" + ? this.getV2DatabaseApiAttributes(projectId, id, key, eventTrigger, region) + : this.getV1DatabaseApiAttributes(projectId, key, eventTrigger); + + logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle)); + + const client = EmulatorRegistry.client(Emulators.DATABASE); + try { + await client.post(apiPath, bundle, { headers: { Authorization: "Bearer owner" } }); + } catch (err: any) { + this.logger.log("WARN", "Error adding Realtime Database function: " + err); + throw err; + } + return true; } - addFirestoreTrigger( + private getV1FirestoreAttributes(projectId: string, key: string, eventTrigger: EventTrigger) { + const bundle = JSON.stringify({ + eventTrigger: { + ...eventTrigger, + service: "firestore.googleapis.com", + }, + }); + const path = `/emulator/v1/projects/${projectId}/triggers/${key}`; + return { bundle, path }; + } + + private getV2FirestoreAttributes(projectId: string, key: string, eventTrigger: EventTrigger) { + logger.debug("Found a v2 firestore trigger."); + const database = eventTrigger.eventFilters?.database; + if (!database) { + throw new FirebaseError("A database must be supplied."); + } + const namespace = eventTrigger.eventFilters?.namespace; + if (!namespace) { + throw new FirebaseError("A namespace must be supplied."); + } + let doc; + let match; + if (eventTrigger.eventFilters?.document) { + doc = eventTrigger.eventFilters?.document; + match = "EXACT"; + } + if (eventTrigger.eventFilterPathPatterns?.document) { + doc = eventTrigger.eventFilterPathPatterns?.document; + match = "PATH_PATTERN"; + } + if (!doc) { + throw new FirebaseError("A document must be supplied."); + } + + const bundle = JSON.stringify({ + eventType: eventTrigger.eventType, + database, + namespace, + document: { + value: doc, + matchType: match, + }, + }); + const path = `/emulator/v1/projects/${projectId}/eventarcTrigger?eventarcTriggerId=${key}`; + return { bundle, path }; + } + + async addFirestoreTrigger( projectId: string, key: string, - eventTrigger: EventTrigger + eventTrigger: EventTrigger, + signature: SignatureType, ): Promise { - const firestoreEmu = EmulatorRegistry.get(Emulators.FIRESTORE); - if (!firestoreEmu) { + if (!EmulatorRegistry.isRunning(Emulators.FIRESTORE)) { return Promise.resolve(false); } - const bundle = JSON.stringify({ eventTrigger }); + const { bundle, path } = + signature === "cloudevent" + ? this.getV2FirestoreAttributes(projectId, key, eventTrigger) + : this.getV1FirestoreAttributes(projectId, key, eventTrigger); + logger.debug(`addFirestoreTrigger`, JSON.stringify(bundle)); - return api - .request("PUT", `/emulator/v1/projects/${projectId}/triggers/${key}`, { - origin: `http://${EmulatorRegistry.getInfoHostString(firestoreEmu.getInfo())}`, - data: bundle, - json: false, - }) - .then(() => { - return true; - }) - .catch((err) => { - this.logger.log("WARN", "Error adding trigger: " + err); - throw err; - }); + const client = EmulatorRegistry.client(Emulators.FIRESTORE); + try { + signature === "cloudevent" ? await client.post(path, bundle) : await client.put(path, bundle); + } catch (err: any) { + this.logger.log("WARN", "Error adding firestore function: " + err); + throw err; + } + return true; } async addPubsubTrigger( @@ -677,19 +982,17 @@ export class FunctionsEmulator implements EmulatorInstance { key: string, eventTrigger: EventTrigger, signatureType: SignatureType, - schedule: EventSchedule | undefined + schedule: EventSchedule | undefined, ): Promise { - const pubsubPort = EmulatorRegistry.getPort(Emulators.PUBSUB); - if (!pubsubPort) { + const pubsubEmulator = EmulatorRegistry.get(Emulators.PUBSUB) as PubsubEmulator | undefined; + if (!pubsubEmulator) { return false; } - const pubsubEmulator = EmulatorRegistry.get(Emulators.PUBSUB) as PubsubEmulator; - logger.debug(`addPubsubTrigger`, JSON.stringify({ eventTrigger })); // "resource":\"projects/{PROJECT_ID}/topics/{TOPIC_ID}"; - const resource = eventTrigger.resource; + const resource = eventTrigger.resource!; let topic; if (schedule) { // In production this topic looks like @@ -704,7 +1007,7 @@ export class FunctionsEmulator implements EmulatorInstance { try { await pubsubEmulator.addTrigger(topic, key, signatureType); return true; - } catch (e) { + } catch (e: any) { return false; } } @@ -722,8 +1025,8 @@ export class FunctionsEmulator implements EmulatorInstance { addStorageTrigger(projectId: string, key: string, eventTrigger: EventTrigger): boolean { logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger })); - const bucket = eventTrigger.resource.startsWith("projects/_/buckets/") - ? eventTrigger.resource.split("/")[3] + const bucket = eventTrigger.resource!.startsWith("projects/_/buckets/") + ? eventTrigger.resource!.split("/")[3] : eventTrigger.resource; const eventTriggerId = `${projectId}:${eventTrigger.eventType}:${bucket}`; const triggers = this.multicastTriggers[eventTriggerId] || []; @@ -732,12 +1035,45 @@ export class FunctionsEmulator implements EmulatorInstance { return true; } + addBlockingTrigger(url: string, blockingTrigger: BlockingTrigger): boolean { + logger.debug(`addBlockingTrigger`, JSON.stringify({ blockingTrigger })); + + const eventType = blockingTrigger.eventType; + if (!AUTH_BLOCKING_EVENTS.includes(eventType as any)) { + return false; + } + + if (blockingTrigger.eventType === BEFORE_CREATE_EVENT) { + this.blockingFunctionsConfig.triggers = { + ...this.blockingFunctionsConfig.triggers, + beforeCreate: { + functionUri: url, + }, + }; + } else { + this.blockingFunctionsConfig.triggers = { + ...this.blockingFunctionsConfig.triggers, + beforeSignIn: { + functionUri: url, + }, + }; + } + + this.blockingFunctionsConfig.forwardInboundCredentials = { + accessToken: !!blockingTrigger.options!.accessToken, + idToken: !!blockingTrigger.options!.idToken, + refreshToken: !!blockingTrigger.options!.refreshToken, + }; + + return true; + } + getProjectId(): string { return this.args.projectId; } getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.FUNCTIONS); + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.FUNCTIONS); return { @@ -755,139 +1091,85 @@ export class FunctionsEmulator implements EmulatorInstance { return Object.values(this.triggers).map((record) => record.def); } - getTriggerDefinitionByKey(triggerKey: string): EmulatedTriggerDefinition { + getTriggerRecordByKey(triggerKey: string): EmulatedTriggerRecord { const record = this.triggers[triggerKey]; if (!record) { logger.debug(`Could not find key=${triggerKey} in ${JSON.stringify(this.triggers)}`); - throw new FirebaseError(`No trigger with key ${triggerKey}`); + throw new FirebaseError(`No function with key ${triggerKey}`); } - return record.def; + return record; } getTriggerKey(def: EmulatedTriggerDefinition): string { // For background triggers we attach the current generation as a suffix - return def.eventTrigger ? `${def.id}-${this.triggerGeneration}` : def.id; + if (def.eventTrigger) { + const triggerKey = `${def.id}-${this.triggerGeneration}`; + return def.eventTrigger.channel ? `${triggerKey}-${def.eventTrigger.channel}` : triggerKey; + } else { + return def.id; + } + } + + getBackendInfo(): BackendInfo[] { + const cf3Triggers = this.getCF3Triggers(); + return this.args.emulatableBackends.map((e: EmulatableBackend) => { + return toBackendInfo(e, cf3Triggers); + }); + } + + getCF3Triggers(): ParsedTriggerDefinition[] { + return Object.values(this.triggers) + .filter((t) => !t.backend.extensionInstanceId) + .map((t) => t.def); } addTriggerRecord( def: EmulatedTriggerDefinition, opts: { ignored: boolean; + backend: EmulatableBackend; url?: string; - } + }, ): void { const key = this.getTriggerKey(def); - this.triggers[key] = { def, enabled: true, ignored: opts.ignored, url: opts.url }; - } - - setTriggersForTesting(triggers: EmulatedTriggerDefinition[]) { - triggers.forEach((def) => this.addTriggerRecord(def, { ignored: false })); - } - - getBaseBundle(): FunctionsRuntimeBundle { - return { - cwd: this.args.functionsDir, - projectId: this.args.projectId, - triggerId: "", - targetName: "", - emulators: { - firestore: EmulatorRegistry.getInfo(Emulators.FIRESTORE), - database: EmulatorRegistry.getInfo(Emulators.DATABASE), - pubsub: EmulatorRegistry.getInfo(Emulators.PUBSUB), - auth: EmulatorRegistry.getInfo(Emulators.AUTH), - storage: EmulatorRegistry.getInfo(Emulators.STORAGE), - }, - adminSdkConfig: { - databaseURL: this.adminSdkConfig.databaseURL, - storageBucket: this.adminSdkConfig.storageBucket, - }, - disabled_features: this.args.disabledRuntimeFeatures, + this.triggers[key] = { + def, + enabled: true, + backend: opts.backend, + ignored: opts.ignored, + url: opts.url, }; } - /** - * Returns a node major version ("10", "8") or null - * @param frb the current Functions Runtime Bundle - */ - getRequestedNodeRuntimeVersion(frb: FunctionsRuntimeBundle): string | undefined { - const pkg = require(path.join(frb.cwd, "package.json")); - return frb.nodeMajorVersion || (pkg.engines && pkg.engines.node); - } - /** - * Returns the path to a "node" executable to use. - * @param cwd the directory to checkout for a package.json file. - * @param nodeMajorVersion forces the emulator to choose this version. Used when emulating extensions, - * since in production, extensions ignore the node version provided in package.json and use the version - * specified in extension.yaml. This will ALWAYS be populated when emulating extensions, even if they - * are using the default version. - */ - askInstallNodeVersion(cwd: string, nodeMajorVersion?: number): string { - const pkg = require(path.join(cwd, "package.json")); - // If the developer hasn't specified a Node to use, inform them that it's an option and use default - if ((!pkg.engines || !pkg.engines.node) && !nodeMajorVersion) { - this.logger.log( - "WARN", - "Your functions directory does not specify a Node version.\n " + - "- Learn more at https://firebase.google.com/docs/functions/manage-functions#set_runtime_options" - ); - return process.execPath; - } - const hostMajorVersion = process.versions.node.split(".")[0]; - const requestedMajorVersion: string = nodeMajorVersion - ? `${nodeMajorVersion}` - : pkg.engines.node; - let localMajorVersion = "0"; - const localNodePath = path.join(cwd, "node_modules/.bin/node"); + setTriggersForTesting(triggers: EmulatedTriggerDefinition[], backend: EmulatableBackend) { + this.triggers = {}; + triggers.forEach((def) => this.addTriggerRecord(def, { backend, ignored: false })); + } - // Next check if we have a Node install in the node_modules folder + getRuntimeConfig(backend: EmulatableBackend): Record { + const configPath = `${backend.functionsDir}/.runtimeconfig.json`; try { - const localNodeOutput = spawnSync(localNodePath, ["--version"]).stdout.toString(); - localMajorVersion = localNodeOutput.slice(1).split(".")[0]; - } catch (err) { - // Will happen if we haven't asked about local version yet - } - - // If the requested version is already locally available, let's use that - if (requestedMajorVersion === localMajorVersion) { - this.logger.logLabeled( - "SUCCESS", - "functions", - `Using node@${requestedMajorVersion} from local cache.` - ); - return localNodePath; - } - - // If the requested version is the same as the host, let's use that - if (requestedMajorVersion === hostMajorVersion) { - this.logger.logLabeled( - "SUCCESS", - "functions", - `Using node@${requestedMajorVersion} from host.` - ); - return process.execPath; + const configContent = fs.readFileSync(configPath, "utf8"); + return JSON.parse(configContent.toString()); + } catch (e) { + // This is fine - runtime config is optional. } - - // Otherwise we'll begin the conversational flow to install the correct version locally - this.logger.log( - "WARN", - `Your requested "node" version "${requestedMajorVersion}" doesn't match your global version "${hostMajorVersion}"` - ); - - return process.execPath; + return {}; } - getUserEnvs(): Record { + getUserEnvs(backend: EmulatableBackend): Record { const projectInfo = { - functionsSource: this.args.functionsDir, + functionsSource: backend.functionsDir, projectId: this.args.projectId, + projectAlias: this.args.projectAlias, isEmulator: true, }; if (functionsEnv.hasUserEnvs(projectInfo)) { try { return functionsEnv.loadUserEnvs(projectInfo); - } catch (e) { + } catch (e: any) { // Ignore - user envs are optional. logger.debug("Failed to load local environment variables", e); } @@ -895,10 +1177,7 @@ export class FunctionsEmulator implements EmulatorInstance { return {}; } - getSystemEnvs(triggerDef?: { - targetName: string; - signatureType: SignatureType; - }): Record { + getSystemEnvs(trigger?: EmulatedTriggerDefinition): Record { const envs: Record = {}; // Env vars guaranteed by GCF platform. @@ -906,13 +1185,15 @@ export class FunctionsEmulator implements EmulatorInstance { envs.GCLOUD_PROJECT = this.args.projectId; envs.K_REVISION = "1"; envs.PORT = "80"; + // Quota project is required when using GCP's Client-based APIs. + // Some GCP client SDKs, like Vertex AI, requires appropriate quota project setup. + envs.GOOGLE_CLOUD_QUOTA_PROJECT = this.args.projectId; - if (triggerDef) { - const service = triggerDef.targetName; - const target = service.replace(/-/g, "."); + if (trigger) { + const target = trigger.entryPoint; envs.FUNCTION_TARGET = target; - envs.FUNCTION_SIGNATURE_TYPE = triggerDef.signatureType; - envs.K_SERVICE = service; + envs.FUNCTION_SIGNATURE_TYPE = getSignatureType(trigger); + envs.K_SERVICE = trigger.name; } return envs; } @@ -923,41 +1204,21 @@ export class FunctionsEmulator implements EmulatorInstance { envs.FUNCTIONS_EMULATOR = "true"; envs.TZ = "UTC"; // Fixes https://github.com/firebase/firebase-tools/issues/2253 envs.FIREBASE_DEBUG_MODE = "true"; - envs.FIREBASE_DEBUG_FEATURES = JSON.stringify({ skipTokenVerification: true }); - - // Make firebase-admin point at the Firestore emulator - const firestoreEmulator = this.getEmulatorInfo(Emulators.FIRESTORE); - if (firestoreEmulator != null) { - envs[Constants.FIRESTORE_EMULATOR_HOST] = formatHost(firestoreEmulator); - } - - // Make firebase-admin point at the Database emulator - const databaseEmulator = this.getEmulatorInfo(Emulators.DATABASE); - if (databaseEmulator) { - envs[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = formatHost(databaseEmulator); - } - - // Make firebase-admin point at the Auth emulator - const authEmulator = this.getEmulatorInfo(Emulators.AUTH); - if (authEmulator) { - envs[Constants.FIREBASE_AUTH_EMULATOR_HOST] = formatHost(authEmulator); - } + envs.FIREBASE_DEBUG_FEATURES = JSON.stringify({ + skipTokenVerification: true, + enableCors: true, + }); - // Make firebase-admin point at the Storage emulator - const storageEmulator = this.getEmulatorInfo(Emulators.STORAGE); - if (storageEmulator) { - envs[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = formatHost(storageEmulator); - // TODO(taeold): We only need FIREBASE_STORAGE_EMULATOR_HOST, as long as the users are using new-ish SDKs. - // Clean up and update documentation in a subsequent patch. - envs[Constants.CLOUD_STORAGE_EMULATOR_HOST] = `http://${formatHost(storageEmulator)}`; + let emulatorInfos = EmulatorRegistry.listRunningWithInfo(); + if (this.args.remoteEmulators) { + emulatorInfos = emulatorInfos.concat(Object.values(this.args.remoteEmulators)); } + setEnvVarsForEmulators(envs, emulatorInfos); - const pubsubEmulator = this.getEmulatorInfo(Emulators.PUBSUB); - if (pubsubEmulator) { - const pubsubHost = formatHost(pubsubEmulator); - process.env.PUBSUB_EMULATOR_HOST = pubsubHost; + if (this.debugMode) { + // Start runtime in debug mode to allow triggers to share single runtime process. + envs["FUNCTION_DEBUG_MODE"] = "true"; } - return envs; } @@ -983,46 +1244,121 @@ export class FunctionsEmulator implements EmulatorInstance { }); } - getRuntimeEnvs(triggerDef?: { - targetName: string; - signatureType: SignatureType; - }): Record { + getRuntimeEnvs( + backend: EmulatableBackend, + trigger?: EmulatedTriggerDefinition, + ): Record { return { - ...this.getUserEnvs(), - ...this.getSystemEnvs(triggerDef), + ...this.getUserEnvs(backend), + ...this.getSystemEnvs(trigger), ...this.getEmulatorEnvs(), FIREBASE_CONFIG: this.getFirebaseConfig(), - ...this.args.env, + ...backend.env, }; } - invokeRuntime( - frb: FunctionsRuntimeBundle, - opts: InvokeRuntimeOpts, - runtimeEnv?: Record - ): RuntimeWorker { - // If we can use an existing worker there is almost nothing to do. - if (this.workerPool.readyForWork(frb.triggerId)) { - return this.workerPool.submitWork(frb.triggerId, frb, opts); + async resolveSecretEnvs( + backend: EmulatableBackend, + trigger?: EmulatedTriggerDefinition, + ): Promise> { + let secretEnvs: Record = {}; + + const secretPath = getSecretLocalPath(backend, this.args.projectDir); + try { + const data = fs.readFileSync(secretPath, "utf8"); + secretEnvs = functionsEnv.parseStrict(data); + } catch (e: any) { + if (e.code !== "ENOENT") { + this.logger.logLabeled( + "ERROR", + "functions", + `Failed to read local secrets file ${secretPath}: ${e.message}`, + ); + } } + // Note - if trigger is undefined, we are loading in 'sequential' mode. + // In that case, we need to load all secrets for that codebase. + const secrets: backend.SecretEnvVar[] = + trigger?.secretEnvironmentVariables || backend.secretEnv; + const accesses = secrets + .filter((s) => !secretEnvs[s.key]) + .map(async (s) => { + this.logger.logLabeled("INFO", "functions", `Trying to access secret ${s.secret}@latest`); + const value = await accessSecretVersion( + this.getProjectId(), + s.secret, + s.version ?? "latest", + ); + return [s.key, value]; + }); + const accessResults = await allSettled(accesses); - const emitter = new EventEmitter(); - const args = [path.join(__dirname, "functionsEmulatorRuntime")]; + const errs: string[] = []; + for (const result of accessResults) { + if (result.status === "rejected") { + errs.push(result.reason as string); + } else { + const [k, v] = result.value; + secretEnvs[k] = v; + } + } - if (opts.ignore_warnings) { - args.unshift("--no-warnings"); + if (errs.length > 0) { + this.logger.logLabeled( + "ERROR", + "functions", + "Unable to access secret environment variables from Google Cloud Secret Manager. " + + "Make sure the credential used for the Functions Emulator have access " + + `or provide override values in ${secretPath}:\n\t` + + errs.join("\n\t"), + ); } - if (this.args.debugPort) { - if (process.env.FIREPIT_VERSION && process.execPath == opts.nodeBinary) { - const requestedMajorNodeVersion = this.getRequestedNodeRuntimeVersion(frb); + return secretEnvs; + } + + async startNode( + backend: EmulatableBackend, + envs: Record, + ): Promise { + const args = [path.join(__dirname, "functionsEmulatorRuntime")]; + if (this.debugMode) { + if (process.env.FIREPIT_VERSION) { this.logger.log( "WARN", - `To enable function inspection, please run "${process.execPath} is:npm i node@${requestedMajorNodeVersion} --save-dev" in your functions directory` + `To enable function inspection, please run "npm i node@${semver.coerce( + backend.runtime || "18.0.0", + )} --save-dev" in your functions directory`, ); } else { + let port: number; + if (typeof this.args.debugPort === "number") { + port = this.args.debugPort; + } else { + // Start the search at port 9229 because that is the default node + // inspector port and Chrome et. al. will discover the process without + // additional configuration. Other dynamic ports will need to be added + // manually to the inspector. + port = await portfinder.getPortPromise({ port: 9229 }); + if (port === 9229) { + this.logger.logLabeled( + "SUCCESS", + "functions", + `Using debug port 9229 for functions codebase ${backend.codebase}`, + ); + } else { + // Give a longer message to warn about non-default ports. + this.logger.logLabeled( + "SUCCESS", + "functions", + `Using debug port ${port} for functions codebase ${backend.codebase}. ` + + "You may need to add manually add this port to your inspector.", + ); + } + } + const { host } = this.getInfo(); - args.unshift(`--inspect=${host}:${this.args.debugPort}`); + args.unshift(`--inspect=${connectableHostname(host)}:${port}`); } } @@ -1030,67 +1366,102 @@ export class FunctionsEmulator implements EmulatorInstance { // module resolution. This feature is mostly incompatible with CF3 (prod or emulated) so // if we detect it we should warn the developer. // See: https://classic.yarnpkg.com/en/docs/pnp/ - const pnpPath = path.join(frb.cwd, ".pnp.js"); + const pnpPath = path.join(backend.functionsDir, ".pnp.js"); if (fs.existsSync(pnpPath)) { EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled( "WARN_ONCE", "functions", "Detected yarn@2 with PnP. " + "Cloud Functions for Firebase requires a node_modules folder to work correctly and is therefore incompatible with PnP. " + - "See https://yarnpkg.com/getting-started/migration#step-by-step for more information." + "See https://yarnpkg.com/getting-started/migration#step-by-step for more information.", ); } - const childProcess = spawn(opts.nodeBinary, args, { - env: { node: opts.nodeBinary, ...process.env, ...(runtimeEnv ?? {}) }, - cwd: frb.cwd, + const bin = backend.bin; + if (!bin) { + throw new Error( + `No binary associated with ${backend.functionsDir}. ` + + "Make sure function runtime is configured correctly in firebase.json.", + ); + } + + const socketPath = getTemporarySocketPath(); + const childProcess = spawn(bin, args, { + cwd: backend.functionsDir, + env: { + node: backend.bin, + METADATA_SERVER_DETECTION: "none", + ...process.env, + ...envs, + PORT: socketPath, + }, stdio: ["pipe", "pipe", "pipe", "ipc"], }); - const buffers: { - [pipe: string]: { - pipe: stream.Readable; - value: string; - }; - } = { - stderr: { pipe: childProcess.stderr, value: "" }, - stdout: { pipe: childProcess.stdout, value: "" }, - }; - - const ipcBuffer = { value: "" }; - childProcess.on("message", (message: any) => { - this.onData(childProcess, emitter, ipcBuffer, message); + return Promise.resolve({ + process: childProcess, + events: new EventEmitter(), + cwd: backend.functionsDir, + conn: new IPCConn(socketPath), }); + } - for (const id in buffers) { - if (buffers.hasOwnProperty(id)) { - const buffer = buffers[id]; - buffer.pipe.on("data", (buf: Buffer) => { - this.onData(childProcess, emitter, buffer, buf); - }); - } + async startPython( + backend: EmulatableBackend, + envs: Record, + ): Promise { + const args = ["functions-framework"]; + + if (this.debugMode) { + this.logger.log("WARN", "--inspect-functions not supported for Python functions. Ignored."); } - const runtime: FunctionsRuntimeInstance = { - pid: childProcess.pid, - exit: new Promise((resolve) => { - childProcess.on("exit", resolve); - }), - events: emitter, - shutdown: () => { - childProcess.kill(); - }, - kill: (signal?: string) => { - childProcess.kill(signal); - emitter.emit("log", new EmulatorLog("SYSTEM", "runtime-status", "killed")); - }, - send: (args: FunctionsRuntimeArgs) => { - return childProcess.send(JSON.stringify(args)); - }, + // No support generic socket interface for Unix Domain Socket/Named Pipe in the python. + // Use TCP/IP stack instead. + const port = await portfinder.getPortPromise({ + port: 8081 + randomInt(0, 1000), // Add a small jitter to avoid race condition. + }); + const childProcess = runWithVirtualEnv(args, backend.functionsDir, { + ...process.env, + ...envs, + // Required to flush stdout/stderr immediately to the piped channels. + PYTHONUNBUFFERED: "1", + // Required to prevent flask development server to reload on code changes. + DEBUG: "False", + HOST: "127.0.0.1", + PORT: port.toString(), + }); + + return { + process: childProcess, + events: new EventEmitter(), + cwd: backend.functionsDir, + conn: new TCPConn("127.0.0.1", port), + }; + } + + async startRuntime( + backend: EmulatableBackend, + trigger?: EmulatedTriggerDefinition, + ): Promise { + const runtimeEnv = this.getRuntimeEnvs(backend, trigger); + const secretEnvs = await this.resolveSecretEnvs(backend, trigger); + + let runtime; + if (backend.runtime!.startsWith("python")) { + runtime = await this.startPython(backend, { ...runtimeEnv, ...secretEnvs }); + } else { + runtime = await this.startNode(backend, { ...runtimeEnv, ...secretEnvs }); + } + const extensionLogInfo = { + instanceId: backend.extensionInstanceId, + ref: backend.extensionVersion?.ref, }; - this.workerPool.addWorker(frb.triggerId, runtime); - return this.workerPool.submitWork(frb.triggerId, frb, opts); + const pool = this.workerPools[backend.codebase]; + const worker = pool.addWorker(trigger, runtime, extensionLogInfo); + await worker.waitForSocketReady(); + return worker; } async disableBackgroundTriggers() { @@ -1099,7 +1470,7 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.logLabeled( "BULLET", `functions[${record.def.entryPoint}]`, - "function temporarily disabled." + "function temporarily disabled.", ); record.enabled = false; } @@ -1110,70 +1481,18 @@ export class FunctionsEmulator implements EmulatorInstance { async reloadTriggers() { this.triggerGeneration++; - return this.loadTriggers(); - } - - private async handleBackgroundTrigger(projectId: string, triggerKey: string, proto: any) { - // If background triggers are disabled, exit early - const record = this.triggers[triggerKey]; - if (record && !record.enabled) { - return Promise.reject({ code: 204, body: "Background triggers are curently disabled." }); + // reset blocking functions config for reloads + this.blockingFunctionsConfig = {}; + for (const backend of this.args.emulatableBackends) { + await this.loadTriggers(backend); } - - const trigger = this.getTriggerDefinitionByKey(triggerKey); - const service = getFunctionService(trigger); - const worker = this.startFunctionRuntime( - trigger.id, - trigger.name, - getSignatureType(trigger), - proto - ); - - return new Promise((resolve, reject) => { - if (projectId !== this.args.projectId) { - // RTDB considers each namespace a "project", but for any other trigger we want to reject - // incoming triggers to a different project. - if (service !== Constants.SERVICE_REALTIME_DATABASE) { - logger.debug( - `Received functions trigger for service "${service}" for unknown project "${projectId}".` - ); - reject({ code: 404 }); - return; - } - - // The eventTrigger 'resource' property will look something like this: - // "projects/_/instances//refs/foo/bar" - // If the trigger's resource does not match the invoked projet ID, we should 404. - if (!trigger.eventTrigger!.resource.startsWith(`projects/_/instances/${projectId}`)) { - logger.debug( - `Received functions trigger for function "${ - trigger.name - }" of project "${projectId}" that did not match definition: ${JSON.stringify(trigger)}.` - ); - reject({ code: 404 }); - return; - } - } - - worker.onLogs((el: EmulatorLog) => { - if (el.level === "FATAL") { - reject({ code: 500, body: el.text }); - } - }); - - // For analytics, track the invoked service - track(EVENT_INVOKE, getFunctionService(trigger)); - - worker.waitForDone().then(() => { - resolve({ status: "acknowledged" }); - }); - }); + await this.performPostLoadOperations(); + return; } /** * Gets the address of a running emulator, either from explicit args or by * consulting the emulator registry. - * * @param emulator */ private getEmulatorInfo(emulator: Emulators): EmulatorInfo | undefined { @@ -1187,7 +1506,7 @@ export class FunctionsEmulator implements EmulatorInstance { } private tokenFromAuthHeader(authHeader: string) { - const match = authHeader.match(/^Bearer (.*)$/); + const match = /^Bearer (.*)$/.exec(authHeader); if (!match) { return; } @@ -1204,7 +1523,7 @@ export class FunctionsEmulator implements EmulatorInstance { } try { - const decoded = jwt.decode(idToken, { complete: true }); + const decoded = jwt.decode(idToken, { complete: true }) as any; if (!decoded || typeof decoded !== "object") { logger.debug(`Failed to decode ID Token: ${decoded}`); return; @@ -1212,36 +1531,51 @@ export class FunctionsEmulator implements EmulatorInstance { // In firebase-functions we manually copy 'sub' to 'uid' // https://github.com/firebase/firebase-admin-node/blob/0b2082f1576f651e75069e38ce87e639c25289af/src/auth/token-verifier.ts#L249 - const claims = decoded.payload; + const claims = decoded.payload as jwt.JwtPayload; claims.uid = claims.sub; return claims; - } catch (e) { + } catch (e: any) { return; } } private async handleHttpsTrigger(req: express.Request, res: express.Response) { const method = req.method; - const region = req.params.region; - const triggerName = req.params.trigger_name; - const triggerId = `${region}-${triggerName}`; + let triggerId: string = req.params.trigger_name; + if (req.params.region) { + triggerId = `${req.params.region}-${triggerId}`; + } if (!this.triggers[triggerId]) { res .status(404) .send( - `Function ${triggerId} does not exist, valid triggers are: ${Object.keys( - this.triggers - ).join(", ")}` + `Function ${triggerId} does not exist, valid functions are: ${Object.keys( + this.triggers, + ).join(", ")}`, ); return; } - const trigger = this.getTriggerDefinitionByKey(triggerId); + const record = this.getTriggerRecordByKey(triggerId); + // If trigger is disabled, exit early + if (!record.enabled) { + res.status(204).send("Background triggers are currently disabled."); + return; + } + const trigger = record.def; logger.debug(`Accepted request ${method} ${req.url} --> ${triggerId}`); - const reqBody = (req as RequestWithRawBody).rawBody; + let reqBody: Buffer | Uint8Array = (req as RequestWithRawBody).rawBody; + // When the payload is a protobuf, EventArc converts a base64 encoded string into a byte array before sending the + // request to the function. Let's mimic that behavior. + if (getSignatureType(trigger) === "cloudevent") { + if (req.headers["content-type"]?.includes("application/protobuf")) { + reqBody = Uint8Array.from(atob(reqBody.toString()), (c) => c.charCodeAt(0)); + req.headers["content-length"] = reqBody.length.toString(); + } + } // For callable functions we want to accept tokens without actually calling verifyIdToken const isCallable = trigger.labels && trigger.labels["deployment-callable"] === "true"; @@ -1260,127 +1594,60 @@ export class FunctionsEmulator implements EmulatorInstance { delete req.headers["authorization"]; req.headers[HttpConstants.CALLABLE_AUTH_HEADER] = encodeURIComponent( - JSON.stringify(contextAuth) + JSON.stringify(contextAuth), ); } } - const worker = this.startFunctionRuntime(trigger.id, trigger.name, "http", undefined); - - worker.onLogs((el: EmulatorLog) => { - if (el.level === "FATAL") { - res.status(500).send(el.text); - } + // For analytics, track the invoked service + void trackEmulator(EVENT_INVOKE_GA4, { + function_service: getFunctionService(trigger), }); - // Wait for the worker to set up its internal HTTP server - await worker.waitForSocketReady(); - - track(EVENT_INVOKE, "https"); - this.logger.log("DEBUG", `[functions] Runtime ready! Sending request!`); - if (!worker.lastArgs) { - throw new FirebaseError("Cannot execute on a worker with no arguments"); - } - - if (!worker.lastArgs.frb.socketPath) { - throw new FirebaseError( - `Cannot execute on a worker without a socketPath: ${JSON.stringify(worker.lastArgs)}` - ); - } - // To match production behavior we need to drop the path prefix // req.url = /:projectId/:region/:trigger_name/* const url = new URL(`${req.protocol}://${req.hostname}${req.url}`); const path = `${url.pathname}${url.search}`.replace( - new RegExp(`\/${this.args.projectId}\/[^\/]*\/${triggerName}\/?`), - "/" + new RegExp(`\/${this.args.projectId}\/[^\/]*\/${req.params.trigger_name}\/?`), + "/", ); // We do this instead of just 302'ing because many HTTP clients don't respect 302s so it may // cause unexpected situations - not to mention CORS troubles and this enables us to use // a socketPath (IPC socket) instead of consuming yet another port which is probably faster as well. this.logger.log("DEBUG", `[functions] Got req.url=${req.url}, mapping to path=${path}`); - const runtimeReq = http.request( + + const pool = this.workerPools[record.backend.codebase]; + if (!pool.readyForWork(trigger.id)) { + try { + await this.startRuntime(record.backend, trigger); + } catch (e: any) { + this.logger.logLabeled("ERROR", `Failed to handle request for function ${trigger.id}`); + this.logger.logLabeled( + "ERROR", + `Failed to start functions in ${record.backend.functionsDir}: ${e}`, + ); + return; + } + } + let debugBundle; + if (this.debugMode) { + debugBundle = { + functionTarget: trigger.entryPoint, + functionSignature: getSignatureType(trigger), + }; + } + await pool.submitRequest( + trigger.id, { method, path, headers: req.headers, - socketPath: worker.lastArgs.frb.socketPath, }, - (runtimeRes: http.IncomingMessage) => { - function forwardStatusAndHeaders(): void { - res.status(runtimeRes.statusCode || 200); - if (!res.headersSent) { - Object.keys(runtimeRes.headers).forEach((key) => { - const val = runtimeRes.headers[key]; - if (val) { - res.setHeader(key, val); - } - }); - } - } - - runtimeRes.on("data", (buf) => { - forwardStatusAndHeaders(); - res.write(buf); - }); - - runtimeRes.on("close", () => { - forwardStatusAndHeaders(); - res.end(); - }); - - runtimeRes.on("end", () => { - forwardStatusAndHeaders(); - res.end(); - }); - } + res as http.ServerResponse, + reqBody, + debugBundle, ); - - runtimeReq.on("error", () => { - res.end(); - }); - - // If the original request had a body, forward that over the connection. - // TODO: Why is this not handled by the pipe? - if (reqBody) { - runtimeReq.write(reqBody); - runtimeReq.end(); - } - - // Pipe the incoming request over the socket. - req.pipe(runtimeReq, { end: true }).on("error", () => { - res.end(); - }); - - await worker.waitForDone(); - } - - private onData( - runtime: ChildProcess, - emitter: EventEmitter, - buffer: { value: string }, - buf: Buffer - ): void { - buffer.value += buf.toString(); - - const lines = buffer.value.split("\n"); - - if (lines.length > 1) { - // slice(0, -1) returns all elements but the last - lines.slice(0, -1).forEach((line: string) => { - const log = EmulatorLog.fromJSON(line); - emitter.emit("log", log); - - if (log.level === "FATAL") { - // Something went wrong, if we don't kill the process it'll wait for timeoutMs. - emitter.emit("log", new EmulatorLog("SYSTEM", "runtime-status", "killed")); - runtime.kill(); - } - }); - } - - buffer.value = lines[lines.length - 1]; } } diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index e9e43496527..8720c3b08ab 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -1,21 +1,6 @@ -import { EmulatorLog } from "./types"; +import * as fs from "fs"; + import { CloudFunction, DeploymentOptions, https } from "firebase-functions"; -import { - ParsedTriggerDefinition, - EmulatedTrigger, - emulatedFunctionsByRegion, - EmulatedTriggerDefinition, - EmulatedTriggerMap, - findModuleRoot, - FunctionsRuntimeBundle, - FunctionsRuntimeFeatures, - getEmulatedTriggersFromDefinitions, - FunctionsRuntimeArgs, - HttpConstants, - getSignatureType, - SignatureType, -} from "./functionsEmulatorShared"; -import { compareVersionStrings } from "./functionsEmulatorUtils"; import * as express from "express"; import * as path from "path"; import * as admin from "firebase-admin"; @@ -23,7 +8,26 @@ import * as bodyParser from "body-parser"; import { pathToFileURL, URL } from "url"; import * as _ from "lodash"; -let triggers: EmulatedTriggerMap | undefined; +import { EmulatorLog } from "./types"; +import { Constants } from "./constants"; +import { + findModuleRoot, + FunctionsRuntimeBundle, + HttpConstants, + SignatureType, +} from "./functionsEmulatorShared"; +import { compareVersionStrings, isLocalHost } from "./functionsEmulatorUtils"; +import { EventUtils } from "./events/types"; + +interface RequestWithRawBody extends express.Request { + rawBody: Buffer; +} + +let functionModule: any; +let FUNCTION_TARGET_NAME: string; +let FUNCTION_SIGNATURE: string; +let FUNCTION_DEBUG_MODE: string; + let developerPkgJSON: PackageJSON | undefined; /** @@ -35,13 +39,6 @@ let developerPkgJSON: PackageJSON | undefined; // eslint-disable-next-line @typescript-eslint/no-implied-eval const dynamicImport = new Function("modulePath", "return import(modulePath)"); -function isFeatureEnabled( - frb: FunctionsRuntimeBundle, - feature: keyof FunctionsRuntimeFeatures -): boolean { - return frb.disabled_features ? !frb.disabled_features[feature] : true; -} - function noOp(): false { return false; } @@ -51,7 +48,7 @@ function requireAsync(moduleName: string, opts?: { paths: string[] }): Promise { try { res(require(require.resolve(moduleName, opts))); // eslint-disable-line @typescript-eslint/no-var-requires - } catch (e) { + } catch (e: any) { rej(e); } }); @@ -61,7 +58,7 @@ function requireResolveAsync(moduleName: string, opts?: { paths: string[] }): Pr return new Promise((res, rej) => { try { res(require.resolve(moduleName, opts)); - } catch (e) { + } catch (e: any) { rej(e); } }); @@ -101,8 +98,8 @@ interface ProxyTarget extends Object { px.when("incremented", (original) => original["value"] + 1); const obj = px.finalize(); - obj.value == 1; - obj.incremented == 2; + obj.value === 1; + obj.incremented === 2; */ class Proxied { /** @@ -137,7 +134,7 @@ class Proxied { proxy: T; private anyValue?: (target: T, key: string) => any; - private appliedValue?: () => any; + private appliedValue?: (...args: any[]) => any; private rewrites: { [key: string]: (target: T, key: string) => any; } = {}; @@ -164,7 +161,7 @@ class Proxied { }, apply: (target, thisArg, argArray) => { if (this.appliedValue) { - return this.appliedValue.apply(thisArg, argArray); + return this.appliedValue.apply(thisArg); } else { return Proxied.applyOriginal(target, thisArg, argArray); } @@ -204,11 +201,8 @@ class Proxied { } } -async function resolveDeveloperNodeModule( - frb: FunctionsRuntimeBundle, - name: string -): Promise { - const pkg = requirePackageJson(frb); +async function resolveDeveloperNodeModule(name: string): Promise { + const pkg = requirePackageJson(); if (!pkg) { new EmulatorLog("SYSTEM", "missing-package-json", "").log(); throw new Error("Could not find package.json"); @@ -224,7 +218,7 @@ async function resolveDeveloperNodeModule( } // Once we know it's in the package.json, make sure it's actually `npm install`ed - const resolveResult = await requireResolveAsync(name, { paths: [frb.cwd] }).catch(noOp); + const resolveResult = await requireResolveAsync(name, { paths: [process.cwd()] }).catch(noOp); if (!resolveResult) { return { declared: true, installed: false }; } @@ -242,30 +236,27 @@ async function resolveDeveloperNodeModule( return moduleResolution; } -async function assertResolveDeveloperNodeModule( - frb: FunctionsRuntimeBundle, - name: string -): Promise { - const resolution = await resolveDeveloperNodeModule(frb, name); +async function assertResolveDeveloperNodeModule(name: string): Promise { + const resolution = await resolveDeveloperNodeModule(name); if ( !(resolution.installed && resolution.declared && resolution.resolution && resolution.version) ) { throw new Error( - `Assertion failure: could not fully resolve ${name}: ${JSON.stringify(resolution)}` + `Assertion failure: could not fully resolve ${name}: ${JSON.stringify(resolution)}`, ); } return resolution as SuccessfulModuleResolution; } -async function verifyDeveloperNodeModules(frb: FunctionsRuntimeBundle): Promise { +async function verifyDeveloperNodeModules(): Promise { const modBundles = [ { name: "firebase-admin", isDev: false, minVersion: "8.9.0" }, { name: "firebase-functions", isDev: false, minVersion: "3.13.1" }, ]; for (const modBundle of modBundles) { - const resolution = await resolveDeveloperNodeModule(frb, modBundle.name); + const resolution = await resolveDeveloperNodeModule(modBundle.name); /* If there's no reference to the module in their package.json, prompt them to install it @@ -292,20 +283,20 @@ async function verifyDeveloperNodeModules(frb: FunctionsRuntimeBundle): Promise< /** * Get the developer's package.json file. */ -function requirePackageJson(frb: FunctionsRuntimeBundle): PackageJSON | undefined { +function requirePackageJson(): PackageJSON | undefined { if (developerPkgJSON) { return developerPkgJSON; } try { - const pkg = require(`${frb.cwd}/package.json`); + const pkg = require(`${process.cwd()}/package.json`); developerPkgJSON = { engines: pkg.engines || {}, dependencies: pkg.dependencies || {}, devDependencies: pkg.devDependencies || {}, }; return developerPkgJSON; - } catch (err) { + } catch (err: any) { return; } } @@ -322,7 +313,7 @@ function requirePackageJson(frb: FunctionsRuntimeBundle): PackageJSON | undefine * * So yeah, we'll try our best and hopefully we can catch 90% of requests. */ -function initializeNetworkFiltering(frb: FunctionsRuntimeBundle): void { +function initializeNetworkFiltering(): void { const networkingModules = [ { name: "http", module: require("http"), path: ["request"] }, { name: "http", module: require("http"), path: ["get"] }, @@ -351,7 +342,7 @@ function initializeNetworkFiltering(frb: FunctionsRuntimeBundle): void { try { new URL(arg); return arg; - } catch (err) { + } catch (err: any) { return; } } else if (typeof arg === "object") { @@ -363,7 +354,7 @@ function initializeNetworkFiltering(frb: FunctionsRuntimeBundle): void { .filter((v) => v); const href = (hrefs.length && hrefs[0]) || ""; - if (href && !history[href] && !href.startsWith("http://localhost")) { + if (href && !history[href] && !isLocalHost(href)) { history[href] = true; if (href.indexOf("googleapis.com") !== -1) { new EmulatorLog("SYSTEM", "googleapis-network-access", "", { @@ -380,7 +371,7 @@ function initializeNetworkFiltering(frb: FunctionsRuntimeBundle): void { try { return original(...args); - } catch (e) { + } catch (e: any) { const newed = new original(...args); // eslint-disable-line new-cap return newed; } @@ -407,21 +398,18 @@ type HttpsHandler = (req: Request, resp: Response) => void; The relevant firebase-functions code is: https://github.com/firebase/firebase-functions/blob/9e3bda13565454543b4c7b2fd10fb627a6a3ab97/src/providers/https.ts#L66 */ -async function initializeFirebaseFunctionsStubs(frb: FunctionsRuntimeBundle): Promise { - const firebaseFunctionsResolution = await assertResolveDeveloperNodeModule( - frb, - "firebase-functions" - ); +async function initializeFirebaseFunctionsStubs(): Promise { + const firebaseFunctionsResolution = await assertResolveDeveloperNodeModule("firebase-functions"); const firebaseFunctionsRoot = findModuleRoot( "firebase-functions", - firebaseFunctionsResolution.resolution + firebaseFunctionsResolution.resolution, ); const httpsProviderResolution = path.join(firebaseFunctionsRoot, "lib/providers/https"); const httpsProviderV1Resolution = path.join(firebaseFunctionsRoot, "lib/v1/providers/https"); let httpsProvider: any; try { httpsProvider = require(httpsProviderV1Resolution); - } catch (e) { + } catch (e: any) { httpsProvider = require(httpsProviderResolution); } @@ -453,7 +441,7 @@ async function initializeFirebaseFunctionsStubs(frb: FunctionsRuntimeBundle): Pr httpsProvider[onCallInnerMethodName] = ( opts: any, handler: any, - deployOpts: DeploymentOptions + deployOpts: DeploymentOptions, ) => { const wrapped = wrapCallableHandler(handler); const cf = onCallMethodOriginal(opts, wrapped, deployOpts); @@ -521,6 +509,38 @@ function getDefaultConfig(): any { return JSON.parse(process.env.FIREBASE_CONFIG || "{}"); } +function initializeRuntimeConfig() { + // Most recent version of Firebase Functions SDK automatically picks up locally + // stored .runtimeconfig.json to populate the config entries. + // However, due to a bug in some older version of the Function SDK, this process may fail. + // + // See the following issues for more detail: + // https://github.com/firebase/firebase-tools/issues/3793 + // https://github.com/firebase/firebase-functions/issues/877 + // + // As a workaround, the emulator runtime will load the contents of the .runtimeconfig.json + // to the CLOUD_RUNTIME_CONFIG environment variable IF the env var is unused. + // In the future, we will bump up the minimum version of the Firebase Functions SDK + // required to run the functions emulator to v3.15.1 and get rid of this workaround. + if (!process.env.CLOUD_RUNTIME_CONFIG) { + const configPath = `${process.cwd()}/.runtimeconfig.json`; + try { + const configContent = fs.readFileSync(configPath, "utf8"); + if (configContent) { + try { + JSON.parse(configContent.toString()); + logDebug(`Found local functions config: ${configPath}`); + process.env.CLOUD_RUNTIME_CONFIG = configContent.toString(); + } catch (e) { + new EmulatorLog("SYSTEM", "function-runtimeconfig-json-invalid", "").log(); + } + } + } catch (e) { + // Ignore, config is optional + } + } +} + /** * This stub is the most important and one of the only non-optional stubs.This feature redirects * writes from the admin SDK back into emulated resources. @@ -530,11 +550,11 @@ function getDefaultConfig(): any { * * We also mock out firestore.settings() so we can merge the emulator settings with the developer's. */ -async function initializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promise { - const adminResolution = await assertResolveDeveloperNodeModule(frb, "firebase-admin"); +async function initializeFirebaseAdminStubs(): Promise { + const adminResolution = await assertResolveDeveloperNodeModule("firebase-admin"); const localAdminModule = require(adminResolution.resolution); - const functionsResolution = await assertResolveDeveloperNodeModule(frb, "firebase-functions"); + const functionsResolution = await assertResolveDeveloperNodeModule("firebase-functions"); const localFunctionsModule = require(functionsResolution.resolution); // Configuration from the environment @@ -556,8 +576,7 @@ async function initializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis }).log(); const defaultApp: admin.app.App = makeProxiedFirebaseApp( - frb, - adminModuleTarget.initializeApp(defaultAppOptions) + adminModuleTarget.initializeApp(defaultAppOptions), ); logDebug("initializeApp(DEFAULT)", defaultAppOptions); @@ -566,12 +585,12 @@ async function initializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis localFunctionsModule.app.setEmulatedAdminApp(defaultApp); // When the auth emulator is running, try to disable JWT verification. - if (frb.emulators.auth) { + if (process.env[Constants.FIREBASE_AUTH_EMULATOR_HOST]) { if (compareVersionStrings(adminResolution.version, "9.3.0") < 0) { new EmulatorLog( "WARN_ONCE", "runtime-status", - "The Firebase Authentication emulator is running, but your 'firebase-admin' dependency is below version 9.3.0, so calls to Firebase Authentication will affect production." + "The Firebase Authentication emulator is running, but your 'firebase-admin' dependency is below version 9.3.0, so calls to Firebase Authentication will affect production.", ).log(); } else if (compareVersionStrings(adminResolution.version, "9.4.2") <= 0) { // Between firebase-admin versions 9.3.0 and 9.4.2 (inclusive) we used the @@ -590,89 +609,108 @@ async function initializeFirebaseAdminStubs(frb: FunctionsRuntimeBundle): Promis return defaultApp; }) .when("firestore", (target) => { - warnAboutFirestoreProd(frb); + warnAboutFirestoreProd(); return Proxied.getOriginal(target, "firestore"); }) .when("database", (target) => { - warnAboutDatabaseProd(frb); + warnAboutDatabaseProd(); return Proxied.getOriginal(target, "database"); }) .when("auth", (target) => { - warnAboutAuthProd(frb); + warnAboutAuthProd(); return Proxied.getOriginal(target, "auth"); }) + .when("storage", (target) => { + warnAboutStorageProd(); + return Proxied.getOriginal(target, "storage"); + }) .finalize(); // Stub the admin module in the require cache - require.cache[adminResolution.resolution] = { + const v = require.cache[adminResolution.resolution]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is not precedent. + require.cache[adminResolution.resolution] = Object.assign(v!, { exports: proxiedAdminModule, path: path.dirname(adminResolution.resolution), - }; + }); logDebug("firebase-admin has been stubbed.", { adminResolution, }); } -function makeProxiedFirebaseApp( - frb: FunctionsRuntimeBundle, - original: admin.app.App -): admin.app.App { +function makeProxiedFirebaseApp(original: admin.app.App): admin.app.App { const appProxy = new Proxied(original); return appProxy .when("firestore", (target: any) => { - warnAboutFirestoreProd(frb); + warnAboutFirestoreProd(); return Proxied.getOriginal(target, "firestore"); }) .when("database", (target: any) => { - warnAboutDatabaseProd(frb); + warnAboutDatabaseProd(); return Proxied.getOriginal(target, "database"); }) .when("auth", (target: any) => { - warnAboutAuthProd(frb); + warnAboutAuthProd(); return Proxied.getOriginal(target, "auth"); }) + .when("storage", (target: any) => { + warnAboutStorageProd(); + return Proxied.getOriginal(target, "storage"); + }) .finalize(); } -function warnAboutFirestoreProd(frb: FunctionsRuntimeBundle): void { - if (frb.emulators.firestore) { +function warnAboutFirestoreProd(): void { + if (process.env[Constants.FIRESTORE_EMULATOR_HOST]) { + return; + } + + new EmulatorLog( + "WARN_ONCE", + "runtime-status", + "The Cloud Firestore emulator is not running, so calls to Firestore will affect production.", + ).log(); +} + +function warnAboutDatabaseProd(): void { + if (process.env[Constants.FIREBASE_DATABASE_EMULATOR_HOST]) { return; } new EmulatorLog( "WARN_ONCE", "runtime-status", - "The Cloud Firestore emulator is not running, so calls to Firestore will affect production." + "The Realtime Database emulator is not running, so calls to Realtime Database will affect production.", ).log(); } -function warnAboutDatabaseProd(frb: FunctionsRuntimeBundle): void { - if (frb.emulators.database) { +function warnAboutAuthProd(): void { + if (process.env[Constants.FIREBASE_AUTH_EMULATOR_HOST]) { return; } new EmulatorLog( "WARN_ONCE", "runtime-status", - "The Realtime Database emulator is not running, so calls to Realtime Database will affect production." + "The Firebase Authentication emulator is not running, so calls to Firebase Authentication will affect production.", ).log(); } -function warnAboutAuthProd(frb: FunctionsRuntimeBundle): void { - if (frb.emulators.auth) { +function warnAboutStorageProd(): void { + if (process.env[Constants.FIREBASE_STORAGE_EMULATOR_HOST]) { return; } new EmulatorLog( "WARN_ONCE", "runtime-status", - "The Firebase Authentication emulator is not running, so calls to Firebase Authentication will affect production." + "The Firebase Storage emulator is not running, so calls to Firebase Storage will affect production.", ).log(); } -async function initializeFunctionsConfigHelper(frb: FunctionsRuntimeBundle): Promise { - const functionsResolution = await assertResolveDeveloperNodeModule(frb, "firebase-functions"); +async function initializeFunctionsConfigHelper(): Promise { + const functionsResolution = await assertResolveDeveloperNodeModule("firebase-functions"); const localFunctionsModule = require(functionsResolution.resolution); logDebug("Checked functions.config()", { @@ -695,34 +733,24 @@ async function initializeFunctionsConfigHelper(frb: FunctionsRuntimeBundle): Pro const functionsModuleProxy = new Proxied(localFunctionsModule); const proxiedFunctionsModule = functionsModuleProxy - .when("config", (target) => () => { + .when("config", () => () => { return proxiedConfig; }) .finalize(); // Stub the functions module in the require cache - require.cache[functionsResolution.resolution] = { + const v = require.cache[functionsResolution.resolution]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is not precedent. + require.cache[functionsResolution.resolution] = Object.assign(v!, { exports: proxiedFunctionsModule, path: path.dirname(functionsResolution.resolution), - }; + }); logDebug("firebase-functions has been stubbed.", { functionsResolution, }); } -/** - * Setup predefined environment variables for Node.js 10 and subsequent runtimes - * https://cloud.google.com/functions/docs/env-var - */ -function setNode10EnvVars(target: string, mode: "event" | "http", service: string) { - process.env.FUNCTION_TARGET = target; - process.env.FUNCTION_SIGNATURE_TYPE = mode; - process.env.K_SERVICE = service; - process.env.K_REVISION = "1"; - process.env.PORT = "80"; -} - /* Retains a reference to the raw body buffer to allow access to the raw body for things like request signature validation. This is used as the "verify" function in body-parser options. @@ -731,106 +759,31 @@ function rawBodySaver(req: express.Request, res: express.Response, buf: Buffer): (req as any).rawBody = buf; } -async function processHTTPS(frb: FunctionsRuntimeBundle, trigger: EmulatedTrigger): Promise { - const ephemeralServer = express(); - const functionRouter = express.Router(); // eslint-disable-line new-cap - const socketPath = frb.socketPath; - - if (!socketPath) { - new EmulatorLog("FATAL", "runtime-error", "Called processHTTPS with no socketPath").log(); - return; - } - - await new Promise((resolveEphemeralServer, rejectEphemeralServer) => { - const handler = async (req: express.Request, res: express.Response) => { - try { - logDebug(`Ephemeral server handling ${req.method} request`); - const func = trigger.getRawFunction(); - res.on("finish", () => { - instance.close((err) => { - if (err) { - rejectEphemeralServer(err); - } else { - resolveEphemeralServer(); - } - }); - }); - - await runHTTPS([req, res], func); - } catch (err) { - rejectEphemeralServer(err); - } - }; - - ephemeralServer.enable("trust proxy"); - ephemeralServer.use( - bodyParser.json({ - limit: "10mb", - verify: rawBodySaver, - }) - ); - ephemeralServer.use( - bodyParser.text({ - limit: "10mb", - verify: rawBodySaver, - }) - ); - ephemeralServer.use( - bodyParser.urlencoded({ - extended: true, - limit: "10mb", - verify: rawBodySaver, - }) - ); - ephemeralServer.use( - bodyParser.raw({ - type: "*/*", - limit: "10mb", - verify: rawBodySaver, - }) - ); - - functionRouter.all("*", handler); - - ephemeralServer.use([`/`, `/*`], functionRouter); - - logDebug(`Attempting to listen to socketPath: ${socketPath}`); - const instance = ephemeralServer.listen(socketPath, () => { - new EmulatorLog("SYSTEM", "runtime-status", "ready", { state: "ready" }).log(); - }); - - instance.on("error", rejectEphemeralServer); - }); -} - async function processBackground( - frb: FunctionsRuntimeBundle, - trigger: EmulatedTrigger, - signature: SignatureType + trigger: CloudFunction, + reqBody: any, + signature: SignatureType, ): Promise { - const proto = frb.proto; - logDebug("ProcessBackground", proto); - if (signature === "cloudevent") { - return runCloudEvent(proto, trigger.getRawFunction()); + return runCloudEvent(trigger, reqBody); } // All formats of the payload should carry a "data" property. The "context" property does // not exist in all versions. Where it doesn't exist, context is everything besides data. - const data = proto.data; - delete proto.data; - const context = proto.context ? proto.context : proto; + const data = reqBody.data; + delete reqBody.data; + const context = reqBody.context ? reqBody.context : reqBody; // This is due to the fact that the Firestore emulator sends payloads in a newer // format than production firestore. - if (!proto.eventType || !proto.eventType.startsWith("google.storage")) { + if (!reqBody.eventType || !reqBody.eventType.startsWith("google.storage")) { if (context.resource && context.resource.name) { logDebug("ProcessBackground: lifting resource.name from resource", context.resource); context.resource = context.resource.name; } } - await runBackground({ data, context }, trigger.getRawFunction()); + await runBackground(trigger, { data, context }); } /** @@ -840,42 +793,37 @@ async function runFunction(func: () => Promise): Promise { let caughtErr; try { await func(); - } catch (err) { + } catch (err: any) { caughtErr = err; } - - logDebug(`Ephemeral server survived.`); if (caughtErr) { throw caughtErr; } } -async function runBackground(proto: any, func: CloudFunction): Promise { - logDebug("RunBackground", proto); +async function runBackground(trigger: CloudFunction, reqBody: any): Promise { + logDebug("RunBackground", reqBody); await runFunction(() => { - return func(proto.data, proto.context); + return trigger(reqBody.data, reqBody.context); }); } -async function runCloudEvent(event: unknown, func: CloudFunction): Promise { +async function runCloudEvent(trigger: CloudFunction, event: unknown): Promise { logDebug("RunCloudEvent", event); await runFunction(() => { - return func(event); + return trigger(event); }); } -async function runHTTPS( - args: any[], - func: (a: express.Request, b: express.Response) => Promise -): Promise { +async function runHTTPS(trigger: CloudFunction, args: any[]): Promise { if (args.length < 2) { throw new Error("Function must be passed 2 args."); } await runFunction(() => { - return func(args[0], args[1]); + return trigger(args[0], args[1]); }); } @@ -883,14 +831,14 @@ async function runHTTPS( This method attempts to help a developer whose code can't be loaded by suggesting possible fixes based on the files in their functions directory. */ -async function moduleResolutionDetective(frb: FunctionsRuntimeBundle, error: Error): Promise { +async function moduleResolutionDetective(error: Error): Promise { /* These files could all potentially exist, if they don't then the value in the map will be falsey, so we just catch to keep from throwing. */ const clues = { - tsconfigJSON: await requireAsync("./tsconfig.json", { paths: [frb.cwd] }).catch(noOp), - packageJSON: await requireAsync("./package.json", { paths: [frb.cwd] }).catch(noOp), + tsconfigJSON: await requireAsync("./tsconfig.json", { paths: [process.cwd()] }).catch(noOp), + packageJSON: await requireAsync("./package.json", { paths: [process.cwd()] }).catch(noOp), }; const isPotentially = { @@ -913,123 +861,65 @@ function logDebug(msg: string, data?: any): void { new EmulatorLog("DEBUG", "runtime-status", `[${process.pid}] ${msg}`, data).log(); } -async function invokeTrigger( - frb: FunctionsRuntimeBundle, - triggers: EmulatedTriggerMap -): Promise { - if (!frb.triggerId) { - throw new Error("frb.triggerId unexpectedly null"); - } - - new EmulatorLog("INFO", "runtime-status", `Beginning execution of "${frb.triggerId}"`, { - frb, - }).log(); - - const trigger = triggers[frb.triggerId]; - logDebug("triggerDefinition", trigger.definition); - const signature = getSignatureType(trigger.definition); +async function initializeRuntime(): Promise { + FUNCTION_DEBUG_MODE = process.env.FUNCTION_DEBUG_MODE || ""; - logDebug(`Running ${frb.triggerId} in signature ${signature}`); - - let seconds = 0; - const timerId = setInterval(() => { - seconds++; - }, 1000); - - let timeoutId; - if (isFeatureEnabled(frb, "timeout")) { - timeoutId = setTimeout(() => { + if (!FUNCTION_DEBUG_MODE) { + FUNCTION_TARGET_NAME = process.env.FUNCTION_TARGET || ""; + if (!FUNCTION_TARGET_NAME) { new EmulatorLog( - "WARN", + "FATAL", "runtime-status", - `Your function timed out after ~${ - trigger.definition.timeout || "60s" - }. To configure this timeout, see - https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation.` + `Environment variable FUNCTION_TARGET cannot be empty. This shouldn't happen.`, ).log(); - throw new Error("Function timed out."); - }, trigger.timeoutMs); - } - - switch (signature) { - case "event": - case "cloudevent": - await processBackground(frb, triggers[frb.triggerId], signature); - break; - case "http": - await processHTTPS(frb, triggers[frb.triggerId]); - break; - } + await flushAndExit(1); + } - if (timeoutId) { - clearTimeout(timeoutId); + FUNCTION_SIGNATURE = process.env.FUNCTION_SIGNATURE_TYPE || ""; + if (!FUNCTION_SIGNATURE) { + new EmulatorLog( + "FATAL", + "runtime-status", + `Environment variable FUNCTION_SIGNATURE_TYPE cannot be empty. This shouldn't happen.`, + ).log(); + await flushAndExit(1); + } } - clearInterval(timerId); - - new EmulatorLog( - "INFO", - "runtime-status", - `Finished "${frb.triggerId}" in ~${Math.max(seconds, 1)}s` - ).log(); -} -async function initializeRuntime( - frb: FunctionsRuntimeBundle, - serializedFunctionTrigger?: string, - extensionTriggers?: ParsedTriggerDefinition[] -): Promise { - logDebug(`Disabled runtime features: ${JSON.stringify(frb.disabled_features)}`); - - const verified = await verifyDeveloperNodeModules(frb); + const verified = await verifyDeveloperNodeModules(); if (!verified) { // If we can't verify the node modules, then just leave, something bad will happen during runtime. new EmulatorLog( "INFO", "runtime-status", - `Your functions could not be parsed due to an issue with your node_modules (see above)` + `Your functions could not be parsed due to an issue with your node_modules (see above)`, ).log(); return; } - initializeNetworkFiltering(frb); - await initializeFunctionsConfigHelper(frb); - await initializeFirebaseFunctionsStubs(frb); - await initializeFirebaseAdminStubs(frb); + initializeRuntimeConfig(); + initializeNetworkFiltering(); + await initializeFunctionsConfigHelper(); + await initializeFirebaseFunctionsStubs(); + await initializeFirebaseAdminStubs(); +} - let parsedDefinitions: ParsedTriggerDefinition[] = []; +async function loadTriggers(): Promise { let triggerModule; - - if (serializedFunctionTrigger) { - /* tslint:disable:no-eval */ - triggerModule = eval(serializedFunctionTrigger)(); - } else { - try { - triggerModule = require(frb.cwd); - } catch (err) { - if (err.code !== "ERR_REQUIRE_ESM") { - await moduleResolutionDetective(frb, err); - return; - } - const modulePath = require.resolve(frb.cwd); - // Resolve module path to file:// URL. Required for windows support. - const moduleURL = pathToFileURL(modulePath).href; - triggerModule = await dynamicImport(moduleURL); + try { + triggerModule = require(process.cwd()); + } catch (err: any) { + if (err.code !== "ERR_REQUIRE_ESM") { + // Try to run diagnostics to see what could've gone wrong before rethrowing the error. + await moduleResolutionDetective(err); + throw err; } + const modulePath = require.resolve(process.cwd()); + // Resolve module path to file:// URL. Required for windows support. + const moduleURL = pathToFileURL(modulePath).href; + triggerModule = await dynamicImport(moduleURL); } - if (extensionTriggers) { - parsedDefinitions = extensionTriggers; - } else { - require("../deploy/functions/runtimes/node/extractTriggers")(triggerModule, parsedDefinitions); - } - - const triggerDefinitions: EmulatedTriggerDefinition[] = emulatedFunctionsByRegion( - parsedDefinitions - ); - - const triggers = getEmulatedTriggersFromDefinitions(triggerDefinitions, triggerModule); - - new EmulatorLog("SYSTEM", "triggers-parsed", "", { triggers, triggerDefinitions }).log(); - return triggers; + return triggerModule; } async function flushAndExit(code: number) { @@ -1037,67 +927,27 @@ async function flushAndExit(code: number) { process.exit(code); } -async function goIdle() { - new EmulatorLog("SYSTEM", "runtime-status", "Runtime is now idle", { state: "idle" }).log(); - await EmulatorLog.waitForFlush(); -} - async function handleMessage(message: string) { - let runtimeArgs: FunctionsRuntimeArgs; + let debug: FunctionsRuntimeBundle["debug"]; try { - runtimeArgs = JSON.parse(message) as FunctionsRuntimeArgs; - } catch (e) { + debug = JSON.parse(message) as FunctionsRuntimeBundle["debug"]; + } catch (e: any) { new EmulatorLog("FATAL", "runtime-error", `Got unexpected message body: ${message}`).log(); await flushAndExit(1); return; } - if (!triggers) { - const serializedTriggers = runtimeArgs.opts ? runtimeArgs.opts.serializedTriggers : undefined; - const extensionTriggers = runtimeArgs.opts ? runtimeArgs.opts.extensionTriggers : undefined; - triggers = await initializeRuntime(runtimeArgs.frb, serializedTriggers, extensionTriggers); - } - - // If we don't have triggers by now, we can't run. - if (!triggers) { - await flushAndExit(1); - return; - } - - // If there's no trigger id it's just a diagnostic call. We can go idle right away. - if (!runtimeArgs.frb.triggerId) { - await goIdle(); - return; - } - - if (!triggers[runtimeArgs.frb.triggerId]) { - new EmulatorLog( - "FATAL", - "runtime-status", - `Could not find trigger "${runtimeArgs.frb.triggerId}" in your functions directory.` - ).log(); - return; - } else { - logDebug(`Trigger "${runtimeArgs.frb.triggerId}" has been found, beginning invocation!`); - } - - try { - await invokeTrigger(runtimeArgs.frb, triggers); - - // If we were passed serialized triggers we have to exit the runtime after, - // otherwise we can go IDLE and await another request. - if (runtimeArgs.opts && runtimeArgs.opts.serializedTriggers) { - await flushAndExit(0); + if (FUNCTION_DEBUG_MODE) { + if (debug) { + FUNCTION_TARGET_NAME = debug.functionTarget; + FUNCTION_SIGNATURE = debug.functionSignature; } else { - await goIdle(); + new EmulatorLog("WARN", "runtime-warning", "Expected debug payload while in debug mode."); } - } catch (err) { - new EmulatorLog("FATAL", "runtime-error", err.stack ? err.stack : err).log(); - await flushAndExit(1); } } -function main(): void { +async function main(): Promise { // Since the functions run as attached processes they naturally inherit SIGINT // sent to the functions emulator. We want them to ignore the first signal // to allow for a clean shutdown. @@ -1117,9 +967,86 @@ function main(): void { } }); - logDebug("Functions runtime initialized.", { - cwd: process.cwd(), - node_version: process.versions.node, + await initializeRuntime(); + try { + functionModule = await loadTriggers(); + } catch (e: any) { + new EmulatorLog( + "FATAL", + "runtime-status", + `Failed to initialize and load triggers. This shouldn't happen: ${e.message}`, + ).log(); + await flushAndExit(1); + } + const app = express(); + app.enable("trust proxy"); + // TODO: This should be 10mb for v1 functions, 32mb for v2, but there is not an easy way to check platform from here. + const bodyParserLimit = "32mb"; + app.use( + bodyParser.json({ + limit: bodyParserLimit, + verify: rawBodySaver, + }), + ); + app.use( + bodyParser.text({ + limit: bodyParserLimit, + verify: rawBodySaver, + }), + ); + app.use( + bodyParser.urlencoded({ + extended: true, + limit: bodyParserLimit, + verify: rawBodySaver, + }), + ); + app.use( + bodyParser.raw({ + type: "*/*", + limit: bodyParserLimit, + verify: rawBodySaver, + }), + ); + app.get("/__/health", (req, res) => { + res.status(200).send(); + }); + app.all("/favicon.ico|/robots.txt", (req, res) => { + res.status(404).send(); + }); + app.all(`/*`, async (req: express.Request, res: express.Response) => { + try { + const trigger = FUNCTION_TARGET_NAME.split(".").reduce((mod, functionTargetPart) => { + return mod?.[functionTargetPart]; + }, functionModule) as CloudFunction; + if (!trigger) { + throw new Error(`Failed to find function ${FUNCTION_TARGET_NAME} in the loaded module`); + } + + switch (FUNCTION_SIGNATURE) { + case "event": + case "cloudevent": + let reqBody; + const rawBody = (req as RequestWithRawBody).rawBody; + if (EventUtils.isBinaryCloudEvent(req)) { + reqBody = EventUtils.extractBinaryCloudEventContext(req); + reqBody.data = req.body; + } else { + reqBody = JSON.parse(rawBody.toString()); + } + await processBackground(trigger, reqBody, FUNCTION_SIGNATURE); + res.send({ status: "acknowledged" }); + break; + case "http": + await runHTTPS(trigger, [req, res]); + } + } catch (err: any) { + new EmulatorLog("FATAL", "runtime-error", err.stack ? err.stack : err).log(); + res.status(500).send(err.message); + } + }); + app.listen(process.env.PORT, () => { + logDebug(`Listening to port: ${process.env.PORT}`); }); // Event emitters do not work well with async functions, so we @@ -1142,5 +1069,15 @@ function main(): void { } if (require.main === module) { - main(); + main() + .then(() => { + logDebug("Functions runtime initialized.", { + cwd: process.cwd(), + node_version: process.versions.node, + }); + }) + .catch((err) => { + new EmulatorLog("FATAL", "runtime-error", err.message || err, err).log(); + return flushAndExit(1); + }); } diff --git a/src/emulator/functionsEmulatorShared.spec.ts b/src/emulator/functionsEmulatorShared.spec.ts new file mode 100644 index 00000000000..c4ecaec9c80 --- /dev/null +++ b/src/emulator/functionsEmulatorShared.spec.ts @@ -0,0 +1,298 @@ +import { expect } from "chai"; +import { BackendInfo, EmulatableBackend } from "./functionsEmulator"; +import * as functionsEmulatorShared from "./functionsEmulatorShared"; +import { + Extension, + ExtensionSpec, + ExtensionVersion, + RegistryLaunchStage, + Visibility, +} from "../extensions/types"; + +const baseDef = { + platform: "gcfv1" as const, + id: "trigger-id", + region: "us-central1", + entryPoint: "fn", + name: "name", +}; + +describe("FunctionsEmulatorShared", () => { + describe(`${functionsEmulatorShared.getFunctionService.name}`, () => { + it("should get service from event trigger definition", () => { + const def = { + ...baseDef, + eventTrigger: { + resource: "projects/my-project/topics/my-topic", + eventType: "google.cloud.pubsub.topic.v1.messagePublished", + service: "pubsub.googleapis.com", + }, + }; + expect(functionsEmulatorShared.getFunctionService(def)).to.be.eql("pubsub.googleapis.com"); + }); + + it("should infer https service from http trigger", () => { + const def = { + ...baseDef, + httpsTrigger: {}, + }; + expect(functionsEmulatorShared.getFunctionService(def)).to.be.eql("https"); + }); + + it("should infer pubsub service based on eventType", () => { + const def = { + ...baseDef, + eventTrigger: { + resource: "projects/my-project/topics/my-topic", + eventType: "google.cloud.pubsub.topic.v1.messagePublished", + }, + }; + expect(functionsEmulatorShared.getFunctionService(def)).to.be.eql("pubsub.googleapis.com"); + }); + + it("should infer firestore service based on eventType", () => { + const def = { + ...baseDef, + eventTrigger: { + resource: "projects/my-project/databases/(default)/documents/my-collection/{docId}", + eventType: "providers/cloud.firestore/eventTypes/document.write", + }, + }; + expect(functionsEmulatorShared.getFunctionService(def)).to.be.eql("firestore.googleapis.com"); + }); + + it("should infer database service based on eventType", () => { + const def = { + ...baseDef, + eventTrigger: { + resource: "projects/_/instances/my-project/refs/messages/{pushId}", + eventType: "providers/google.firebase.database/eventTypes/ref.write", + }, + }; + expect(functionsEmulatorShared.getFunctionService(def)).to.be.eql("firebaseio.com"); + }); + + it("should infer storage service based on eventType", () => { + const def = { + ...baseDef, + eventTrigger: { + resource: "projects/_/buckets/mybucket", + eventType: "google.storage.object.finalize", + }, + }; + expect(functionsEmulatorShared.getFunctionService(def)).to.be.eql("storage.googleapis.com"); + }); + + it("should infer auth service based on eventType", () => { + const def = { + ...baseDef, + eventTrigger: { + resource: "projects/my-project", + eventType: "providers/firebase.auth/eventTypes/user.create", + }, + }; + expect(functionsEmulatorShared.getFunctionService(def)).to.be.eql( + "firebaseauth.googleapis.com", + ); + }); + }); + + describe(`${functionsEmulatorShared.getSecretLocalPath.name}`, () => { + const testProjectDir = "project/dir"; + const tests: { + desc: string; + in: EmulatableBackend; + expected: string; + }[] = [ + { + desc: "should return the correct location for an Extension backend", + in: { + functionsDir: "extensions/functions", + env: {}, + secretEnv: [], + extensionInstanceId: "my-extension-instance", + codebase: "", + }, + expected: "project/dir/extensions/my-extension-instance.secret.local", + }, + { + desc: "should return the correct location for a CF3 backend", + in: { + functionsDir: "test/cf3", + env: {}, + secretEnv: [], + codebase: "", + }, + expected: "test/cf3/.secret.local", + }, + ]; + + for (const t of tests) { + it(t.desc, () => { + expect(functionsEmulatorShared.getSecretLocalPath(t.in, testProjectDir)).to.equal( + t.expected, + ); + }); + } + }); + + describe(`${functionsEmulatorShared.toBackendInfo.name}`, () => { + const testCF3Triggers: functionsEmulatorShared.ParsedTriggerDefinition[] = [ + { + entryPoint: "cf3", + platform: "gcfv1", + name: "cf3-trigger", + codebase: "", + }, + ]; + const testExtTriggers: functionsEmulatorShared.ParsedTriggerDefinition[] = [ + { + entryPoint: "ext", + platform: "gcfv1", + name: "ext-trigger", + }, + ]; + const testSpec: ExtensionSpec = { + name: "my-extension", + version: "0.1.0", + resources: [], + sourceUrl: "test.com", + params: [], + systemParams: [], + postinstallContent: "Should subsitute ${param:KEY}", + }; + const testSubbedSpec: ExtensionSpec = { + name: "my-extension", + version: "0.1.0", + resources: [], + sourceUrl: "test.com", + params: [], + systemParams: [], + postinstallContent: "Should subsitute value", + }; + const testExtension: Extension = { + name: "my-extension", + ref: "pubby/my-extensions", + state: "PUBLISHED", + createTime: "", + visibility: Visibility.PUBLIC, + registryLaunchStage: RegistryLaunchStage.BETA, + }; + const testExtensionVersion = (spec: ExtensionSpec): ExtensionVersion => { + return { + name: "my-extension", + ref: "pubby/my-extensions@0.1.0", + state: "PUBLISHED", + spec, + hash: "abc123", + sourceDownloadUri: "test.com", + }; + }; + + const tests: { + desc: string; + in: EmulatableBackend; + expected: BackendInfo; + }[] = [ + { + desc: "should transform a published Extension backend", + in: { + functionsDir: "test", + env: { + KEY: "value", + }, + secretEnv: [], + predefinedTriggers: testExtTriggers, + extension: testExtension, + extensionVersion: testExtensionVersion(testSpec), + extensionInstanceId: "my-instance", + codebase: "", + }, + expected: { + directory: "test", + env: { + KEY: "value", + }, + functionTriggers: testExtTriggers, + extension: testExtension, + extensionVersion: testExtensionVersion(testSubbedSpec), + extensionInstanceId: "my-instance", + }, + }, + { + desc: "should transform a local Extension backend", + in: { + functionsDir: "test", + env: { + KEY: "value", + }, + secretEnv: [], + predefinedTriggers: testExtTriggers, + extensionSpec: testSpec, + extensionInstanceId: "my-local-instance", + codebase: "", + }, + expected: { + directory: "test", + env: { + KEY: "value", + }, + functionTriggers: testExtTriggers, + extensionSpec: testSubbedSpec, + extensionInstanceId: "my-local-instance", + }, + }, + { + desc: "should transform a CF3 backend", + in: { + functionsDir: "test", + env: { + KEY: "value", + }, + secretEnv: [], + codebase: "", + }, + expected: { + directory: "test", + env: { + KEY: "value", + }, + functionTriggers: testCF3Triggers, + }, + }, + { + desc: "should add secretEnvVar into env", + in: { + functionsDir: "test", + env: { + KEY: "value", + }, + secretEnv: [ + { + key: "secret", + secret: "asecret", + projectId: "test", + }, + ], + codebase: "", + }, + expected: { + directory: "test", + env: { + KEY: "value", + secret: "projects/test/secrets/asecret/versions/latest", + }, + functionTriggers: testCF3Triggers, + }, + }, + ]; + + for (const tc of tests) { + it(tc.desc, () => { + expect(functionsEmulatorShared.toBackendInfo(tc.in, testCF3Triggers)).to.deep.equal( + tc.expected, + ); + }); + } + }); +}); diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index c41c568e5a4..3ff9e1955c2 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -1,32 +1,64 @@ -import * as _ from "lodash"; -import { CloudFunction } from "firebase-functions"; import * as os from "os"; import * as path from "path"; -import * as express from "express"; import * as fs from "fs"; +import { randomBytes } from "crypto"; +import * as _ from "lodash"; +import * as express from "express"; +import { CloudFunction } from "firebase-functions"; +import * as backend from "../deploy/functions/backend"; import { Constants } from "./constants"; -import { InvokeRuntimeOpts } from "./functionsEmulator"; -import { FunctionsPlatform } from "../deploy/functions/backend"; +import { BackendInfo, EmulatableBackend, InvokeRuntimeOpts } from "./functionsEmulator"; +import { ENV_DIRECTORY } from "../extensions/manifest"; +import { substituteParams } from "../extensions/extensionsHelper"; +import { ExtensionSpec, ExtensionVersion } from "../extensions/types"; +import { replaceConsoleLinks } from "./extensions/postinstall"; +import { serviceForEndpoint } from "../deploy/functions/services"; +import { inferBlockingDetails } from "../deploy/functions/prepare"; +import * as events from "../functions/events"; +import { connectableHostname } from "../utils"; + +/** The current v2 events that are implemented in the emulator */ +const V2_EVENTS = [ + events.v2.PUBSUB_PUBLISH_EVENT, + ...events.v2.STORAGE_EVENTS, + ...events.v2.DATABASE_EVENTS, + ...events.v2.FIRESTORE_EVENTS, +]; + +/** + * Label for eventarc event sources. + * TODO: Consider DRYing from functions/prepare.ts + * A nice place would be to put it in functionsv2.ts once we get rid of functions.ts + */ +export const EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE"; export type SignatureType = "http" | "event" | "cloudevent"; export interface ParsedTriggerDefinition { entryPoint: string; - platform: FunctionsPlatform; + platform: backend.FunctionsPlatform; name: string; - timeout?: string | number; // Can be "3s" for some reason lol + timeoutSeconds?: number; regions?: string[]; - availableMemoryMb?: "128MB" | "256MB" | "512MB" | "1GB" | "2GB" | "4GB"; + availableMemoryMb?: backend.MemoryOptions; httpsTrigger?: any; eventTrigger?: EventTrigger; schedule?: EventSchedule; + blockingTrigger?: BlockingTrigger; labels?: { [key: string]: any }; + codebase?: string; } export interface EmulatedTriggerDefinition extends ParsedTriggerDefinition { id: string; // An unique-id per-function, generated from the name and the region. region: string; + secretEnvironmentVariables?: backend.SecretEnvVar[]; // Secret env vars needs to be specially loaded in the Emulator. +} + +export interface BlockingTrigger { + eventType: string; + options?: Record; } export interface EventSchedule { @@ -35,8 +67,11 @@ export interface EventSchedule { } export interface EventTrigger { - resource: string; + resource?: string; eventType: string; + channel?: string; + eventFilters?: Record; + eventFilterPathPatterns?: Record; // Deprecated service?: string; } @@ -51,55 +86,24 @@ export interface FunctionsRuntimeArgs { } export interface FunctionsRuntimeBundle { - projectId: string; - proto?: any; - triggerId?: string; - targetName?: string; - emulators: { - firestore?: { - host: string; - port: number; - }; - database?: { - host: string; - port: number; - }; - pubsub?: { - host: string; - port: number; - }; - auth?: { - host: string; - port: number; - }; - storage?: { - host: string; - port: number; - }; - }; - adminSdkConfig: { - databaseURL?: string; - storageBucket?: string; - }; - socketPath?: string; + proto: any; disabled_features?: FunctionsRuntimeFeatures; - nodeMajorVersion?: number; - cwd: string; + // TODO(danielylee): To make debugging in Functions Emulator w/ --inspect-functions flag a good experience, we run + // all functions in a single runtime process. This is drastically different to production environment where each + // function runs in isolated, independent containers. Until we have better design for supporting --inspect-functions + // flag, we begrudgingly include the target trigger info in the runtime bundle so the "debug" runtime process can + // choose which trigger to run at runtime. + // See https://github.com/firebase/firebase-tools/issues/4189. + debug?: { + functionTarget: string; + functionSignature: string; + }; } export interface FunctionsRuntimeFeatures { timeout?: boolean; } -const memoryLookup = { - "128MB": 128, - "256MB": 256, - "512MB": 512, - "1GB": 1024, - "2GB": 2048, - "4GB": 4096, -}; - export class HttpConstants { static readonly CALLABLE_AUTH_HEADER: string = "x-callable-context-auth"; static readonly ORIGINAL_AUTH_HEADER: string = "x-original-auth"; @@ -111,18 +115,17 @@ export class EmulatedTrigger { the actual module which contains multiple functions / definitions. We locate the one we need below using definition.entryPoint */ - constructor(public definition: EmulatedTriggerDefinition, private module: any) {} + constructor( + public definition: EmulatedTriggerDefinition, + private module: any, + ) {} get memoryLimitBytes(): number { - return memoryLookup[this.definition.availableMemoryMb || "128MB"] * 1024 * 1024; + return (this.definition.availableMemoryMb || 128) * 1024 * 1024; } get timeoutMs(): number { - if (typeof this.definition.timeout === "number") { - return this.definition.timeout * 1000; - } else { - return parseInt((this.definition.timeout || "60s").split("s")[0], 10) * 1000; - } + return (this.definition.timeoutSeconds || 60) * 1000; } getRawFunction(): CloudFunction { @@ -135,13 +138,128 @@ export class EmulatedTrigger { } } +/** + * Checks if the v2 event service has been implemented in the emulator + */ +export function eventServiceImplemented(eventType: string): boolean { + return V2_EVENTS.includes(eventType); +} + +/** + * Validates that triggers are correctly formed and fills in some defaults. + */ +export function prepareEndpoints(endpoints: backend.Endpoint[]) { + const bkend = backend.of(...endpoints); + for (const ep of endpoints) { + serviceForEndpoint(ep).validateTrigger(ep as any, bkend); + } + inferBlockingDetails(bkend); +} + +/** + * Creates a unique trigger definition from Endpoints. + * @param Endpoints A list of all CloudFunctions in the deployment. + * @return A list of all CloudFunctions in the deployment. + */ +export function emulatedFunctionsFromEndpoints( + endpoints: backend.Endpoint[], +): EmulatedTriggerDefinition[] { + const regionDefinitions: EmulatedTriggerDefinition[] = []; + for (const endpoint of endpoints) { + if (!endpoint.region) { + endpoint.region = "us-central1"; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const def: EmulatedTriggerDefinition = { + entryPoint: endpoint.entryPoint, + platform: endpoint.platform, + region: endpoint.region, + // TODO: Difference in use of name/id in Endpoint vs Emulator is subtle and confusing. + // We should later refactor the emulator to stop using a custom trigger definition. + name: endpoint.id, + id: `${endpoint.region}-${endpoint.id}`, + codebase: endpoint.codebase, + }; + def.availableMemoryMb = endpoint.availableMemoryMb || 256; + def.labels = endpoint.labels || {}; + if (endpoint.platform === "gcfv1") { + def.labels[EVENTARC_SOURCE_ENV] = + "cloudfunctions-emulated.googleapis.com" + + `/projects/${endpoint.project || "project"}/locations/${endpoint.region}/functions/${ + endpoint.id + }`; + } else if (endpoint.platform === "gcfv2") { + def.labels[EVENTARC_SOURCE_ENV] = + "run-emulated.googleapis.com" + + `/projects/${endpoint.project || "project"}/locations/${endpoint.region}/services/${ + endpoint.id + }`; + } + def.timeoutSeconds = endpoint.timeoutSeconds || 60; + def.secretEnvironmentVariables = endpoint.secretEnvironmentVariables || []; + def.platform = endpoint.platform; + // TODO: This transformation is confusing but must be kept since the Firestore/RTDB trigger registration + // process requires it in this form. Need to work in Firestore emulator for a proper fix... + if (backend.isHttpsTriggered(endpoint)) { + def.httpsTrigger = endpoint.httpsTrigger; + } else if (backend.isCallableTriggered(endpoint)) { + def.httpsTrigger = {}; + def.labels = { ...def.labels, "deployment-callable": "true" }; + } else if (backend.isEventTriggered(endpoint)) { + const eventTrigger = endpoint.eventTrigger; + if (endpoint.platform === "gcfv1") { + def.eventTrigger = { + eventType: eventTrigger.eventType, + resource: eventTrigger.eventFilters!.resource, + }; + } else { + // TODO(colerogers): v2 events implemented are pubsub, storage, rtdb, and custom events + if (!eventServiceImplemented(eventTrigger.eventType) && !eventTrigger.channel) { + continue; + } + + // We use resource for pubsub & storage + const { resource, topic, bucket } = endpoint.eventTrigger.eventFilters as any; + const eventResource = resource || topic || bucket; + + def.eventTrigger = { + eventType: eventTrigger.eventType, + resource: eventResource, + channel: eventTrigger.channel, + eventFilters: eventTrigger.eventFilters, + eventFilterPathPatterns: eventTrigger.eventFilterPathPatterns, + }; + } + } else if (backend.isScheduleTriggered(endpoint)) { + // TODO: This is an awkward transformation. Emulator does not understand scheduled triggers - maybe it should? + def.eventTrigger = { eventType: "pubsub", resource: "" }; + def.schedule = endpoint.scheduleTrigger as EventSchedule; + } else if (backend.isBlockingTriggered(endpoint)) { + def.blockingTrigger = { + eventType: endpoint.blockingTrigger.eventType, + options: endpoint.blockingTrigger.options || {}, + }; + } else if (backend.isTaskQueueTriggered(endpoint)) { + // Just expose TQ trigger as HTTPS. Useful for debugging. + def.httpsTrigger = {}; + } else { + // All other trigger types are not supported by the emulator + // We leave both eventTrigger and httpTrigger attributes empty + // and let the caller deal with invalid triggers. + } + regionDefinitions.push(def); + } + return regionDefinitions; +} + /** * Creates a unique trigger definition for each region a function is defined in. * @param definitions A list of all CloudFunctions in the deployment. * @return A list of all CloudFunctions in the deployment, with copies for each region. */ export function emulatedFunctionsByRegion( - definitions: ParsedTriggerDefinition[] + definitions: ParsedTriggerDefinition[], + secretEnvVariables: backend.SecretEnvVar[] = [], ): EmulatedTriggerDefinition[] { const regionDefinitions: EmulatedTriggerDefinition[] = []; for (const def of definitions) { @@ -157,6 +275,7 @@ export function emulatedFunctionsByRegion( defDeepCopy.region = region; defDeepCopy.id = `${region}-${defDeepCopy.name}`; defDeepCopy.platform = defDeepCopy.platform || "gcfv1"; + defDeepCopy.secretEnvironmentVariables = secretEnvVariables; regionDefinitions.push(defDeepCopy); } @@ -167,23 +286,26 @@ export function emulatedFunctionsByRegion( /** * Converts an array of EmulatedTriggerDefinitions to a map of EmulatedTriggers, which contain information on execution, * @param {EmulatedTriggerDefinition[]} definitions An array of regionalized, parsed trigger definitions - * @param {Object} module Actual module which contains multiple functions / definitions + * @param {object} module Actual module which contains multiple functions / definitions * @return a map of trigger ids to EmulatedTriggers */ export function getEmulatedTriggersFromDefinitions( definitions: EmulatedTriggerDefinition[], - module: any // eslint-disable-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + module: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any ): EmulatedTriggerMap { return definitions.reduce( (obj: { [triggerName: string]: EmulatedTrigger }, definition: EmulatedTriggerDefinition) => { obj[definition.id] = new EmulatedTrigger(definition, module); return obj; }, - {} + {}, ); } -export function getTemporarySocketPath(pid: number, cwd: string): string { +/** + * Create a path that used to create a tempfile for IPC over socket files. + */ +export function getTemporarySocketPath(): string { // See "net" package docs for information about IPC pipes on Windows // https://nodejs.org/api/net.html#net_identifying_paths_for_ipc_connections // @@ -197,21 +319,41 @@ export function getTemporarySocketPath(pid: number, cwd: string): string { // /var/folders/xl/6lkrzp7j07581mw8_4dlt3b000643s/T/{...}.sock // Since the system prefix is about ~50 chars we only have about ~50 more to work with // before we will get truncated socket names and then undefined behavior. + const rand = randomBytes(8).toString("hex"); if (process.platform === "win32") { - return path.join("\\\\?\\pipe", cwd, pid.toString()); + return path.join("\\\\?\\pipe", `fire_emu_${rand}`); } else { - return path.join(os.tmpdir(), `fire_emu_${pid.toString()}.sock`); + return path.join(os.tmpdir(), `fire_emu_${rand}.sock`); } } -export function getFunctionService(def: EmulatedTriggerDefinition): string { +/** + * In GCF 1st gen, there was a mostly undocumented "service" field + * which identified where an event was coming from. This is used in the emulator + * to determine which emulator serves these triggers. Now that GCF 2nd gen + * discontinued the "service" field this becomes more bespoke. + */ +export function getFunctionService(def: ParsedTriggerDefinition): string { if (def.eventTrigger) { + if (def.eventTrigger.channel) { + return Constants.SERVICE_EVENTARC; + } return def.eventTrigger.service ?? getServiceFromEventType(def.eventTrigger.eventType); } + if (def.blockingTrigger) { + return def.blockingTrigger.eventType; + } + if (def.httpsTrigger) { + return "https"; + } return "unknown"; } +/** + * Returns a service ID to use for GCF 2nd gen events. Used to connect the right + * emulator service. + */ export function getServiceFromEventType(eventType: string): string { if (eventType.includes("firestore")) { return Constants.SERVICE_FIRESTORE; @@ -245,6 +387,9 @@ export function getServiceFromEventType(eventType: string): string { return ""; } +/** + * Create a Promise which can be awaited to recieve request bodies as strings. + */ export function waitForBody(req: express.Request): Promise { let data = ""; return new Promise((resolve) => { @@ -258,6 +403,9 @@ export function waitForBody(req: express.Request): Promise { }); } +/** + * Find the root directory housing a node module. + */ export function findModuleRoot(moduleName: string, filepath: string): string { const hierarchy = filepath.split(path.sep); @@ -275,7 +423,7 @@ export function findModuleRoot(moduleName: string, filepath: string): string { return chunks.join("/"); } break; - } catch (err) { + } catch (err: any) { /**/ } } @@ -283,16 +431,33 @@ export function findModuleRoot(moduleName: string, filepath: string): string { return ""; } +/** + * Format a hostname for TCP dialing. Should only be used in Functions emulator. + * + * This is similar to EmulatorRegistry.url but with no explicit dependency on + * the registry and so on and thus can work in functions shell. + * + * For any other part of the CLI, please use EmulatorRegistry.url(...).host + * instead, which handles discovery, formatting, and fixing host in one go. + */ export function formatHost(info: { host: string; port: number }): string { - if (info.host.includes(":")) { - return `[${info.host}]:${info.port}`; + const host = connectableHostname(info.host); + if (host.includes(":")) { + return `[${host}]:${info.port}`; } else { - return `${info.host}:${info.port}`; + return `${host}:${info.port}`; } } +/** + * Determines the correct value for the environment variable that tells the + * Functions Framework how to parse this functions' input. + */ export function getSignatureType(def: EmulatedTriggerDefinition): SignatureType { - if (def.httpsTrigger) { + if (def.httpsTrigger || def.blockingTrigger) { + return "http"; + } + if (def.platform === "gcfv2" && def.schedule) { return "http"; } // TODO: As implemented, emulated CF3v1 functions cannot receive events in CloudEvent format, and emulated CF3v2 @@ -300,3 +465,64 @@ export function getSignatureType(def: EmulatedTriggerDefinition): SignatureType // that allows CF3v1 functions to target GCFv2 and vice versa. return def.platform === "gcfv2" ? "cloudevent" : "event"; } + +const LOCAL_SECRETS_FILE = ".secret.local"; + +/** + * getSecretLocalPath returns the expected location for a .secret.local override file. + */ +export function getSecretLocalPath(backend: EmulatableBackend, projectDir: string) { + const secretsFile = backend.extensionInstanceId + ? `${backend.extensionInstanceId}${LOCAL_SECRETS_FILE}` + : LOCAL_SECRETS_FILE; + const secretDirectory = backend.extensionInstanceId + ? path.join(projectDir, ENV_DIRECTORY) + : backend.functionsDir; + return path.join(secretDirectory, secretsFile); +} + +/** + * toBackendInfo transforms an EmulatableBackend into its correspondign API type, BackendInfo + * @param e the emulatableBackend to transform + * @param cf3Triggers a list of CF3 triggers. If e does not include predefinedTriggers, these will be used instead. + */ +export function toBackendInfo( + e: EmulatableBackend, + cf3Triggers: ParsedTriggerDefinition[], +): BackendInfo { + const envWithSecrets = Object.assign({}, e.env); + for (const s of e.secretEnv) { + envWithSecrets[s.key] = backend.secretVersionName(s); + } + let extensionVersion = e.extensionVersion; + if (extensionVersion) { + extensionVersion = substituteParams(extensionVersion, e.env); + if (extensionVersion.spec?.postinstallContent) { + extensionVersion.spec.postinstallContent = replaceConsoleLinks( + extensionVersion.spec.postinstallContent, + ); + } + } + let extensionSpec = e.extensionSpec; + if (extensionSpec) { + extensionSpec = substituteParams(extensionSpec, e.env); + if (extensionSpec?.postinstallContent) { + extensionSpec.postinstallContent = replaceConsoleLinks(extensionSpec.postinstallContent); + } + } + + // Parse and stringify to get rid of undefined values + return JSON.parse( + JSON.stringify({ + directory: e.functionsDir, + env: envWithSecrets, + extensionInstanceId: e.extensionInstanceId, // Present on all extensions + extension: e.extension, // Only present on published extensions + extensionVersion: extensionVersion, // Only present on published extensions + extensionSpec: extensionSpec, // Only present on local extensions + functionTriggers: + // If we don't have predefinedTriggers, this is the CF3 backend. + e.predefinedTriggers ?? cf3Triggers.filter((t) => t.codebase === e.codebase), + }), + ); +} diff --git a/src/emulator/functionsEmulatorShell.ts b/src/emulator/functionsEmulatorShell.ts index 054b4e3ed48..5b1ff7a4c58 100644 --- a/src/emulator/functionsEmulatorShell.ts +++ b/src/emulator/functionsEmulatorShell.ts @@ -1,17 +1,14 @@ import * as uuid from "uuid"; -import { FunctionsEmulator } from "./functionsEmulator"; -import { - EmulatedTriggerDefinition, - getSignatureType, - SignatureType, -} from "./functionsEmulatorShared"; + import * as utils from "../utils"; +import { FunctionsEmulator } from "./functionsEmulator"; +import { EmulatedTriggerDefinition } from "./functionsEmulatorShared"; import { logger } from "../logger"; import { FirebaseError } from "../error"; -import { LegacyEvent } from "./events/types"; +import { CloudEvent, EventOptions, LegacyEvent } from "./events/types"; interface FunctionsShellController { - call(name: string, data: any, opts: any): void; + call(trigger: EmulatedTriggerDefinition, data: any, opts: any): void; } export class FunctionsEmulatorShell implements FunctionsShellController { @@ -29,57 +26,86 @@ export class FunctionsEmulatorShell implements FunctionsShellController { for (const trigger of this.triggers) { if (trigger.httpsTrigger) { this.urls[trigger.id] = FunctionsEmulator.getHttpFunctionUrl( - this.emu.getInfo().host, - this.emu.getInfo().port, this.emu.getProjectId(), trigger.name, - trigger.region + trigger.region, + this.emu.getInfo(), // EmulatorRegistry is not available in shell ); } } } - call(name: string, data: any, opts: any): void { - const trigger = this.getTrigger(name); - logger.debug(`shell:${name}: trigger=${JSON.stringify(trigger)}`); - logger.debug(`shell:${name}: opts=${JSON.stringify(opts)}, data=${JSON.stringify(data)}`); - - if (!trigger.eventTrigger) { - throw new FirebaseError(`Function ${name} is not a background function`); - } - - const eventType = trigger.eventTrigger.eventType; - + private createLegacyEvent( + eventTrigger: Required["eventTrigger"], + data: unknown, + opts: EventOptions, + ): LegacyEvent { // Resource could either be 'string' or '{ name: string, service: string }' let resource = opts.resource; if (typeof resource === "object" && resource.name) { resource = resource.name; } - - // TODO: We always use v1beta1 events for now, but we want to move - // to v1beta2 as soon as we can. - const proto: LegacyEvent = { + return { eventId: uuid.v4(), timestamp: new Date().toISOString(), - eventType, - resource, + eventType: eventTrigger.eventType, + resource: resource as string, params: opts.params, - auth: opts.auth, + auth: { admin: opts.auth?.admin || false, variable: opts.auth?.variable }, data, }; + } - this.emu.startFunctionRuntime(trigger.id, trigger.name, getSignatureType(trigger), proto); + private createCloudEvent( + eventTrigger: Required["eventTrigger"], + data: unknown, + opts: EventOptions, + ): CloudEvent { + const ce: CloudEvent = { + specversion: "1.0", + datacontenttype: "application/json", + id: uuid.v4(), + type: eventTrigger.eventType, + time: new Date().toISOString(), + source: "", + data, + }; + if (eventTrigger.eventType.startsWith("google.cloud.storage")) { + ce.source = `projects/_/buckets/${eventTrigger.eventFilters?.bucket}`; + } else if (eventTrigger.eventType.startsWith("google.cloud.pubsub")) { + ce.source = eventTrigger.eventFilters!.topic!; + data = { ...(data as any), messageId: uuid.v4() }; + } else if (eventTrigger.eventType.startsWith("google.cloud.firestore")) { + ce.source = `projects/_/databases/(default)`; + if (opts.resource) { + ce.document = opts.resource as string; + } + } else if (eventTrigger.eventType.startsWith("google.firebase.database")) { + ce.source = `projects/_/locations/_/instances/${eventTrigger.eventFilterPathPatterns?.instance}`; + if (opts.resource) { + ce.ref = opts.resource as string; + } + } + return ce; } - private getTrigger(name: string): EmulatedTriggerDefinition { - const result = this.triggers.find((trigger) => { - return trigger.name === name; - }); + call(trigger: EmulatedTriggerDefinition, data: any, opts: EventOptions): void { + logger.debug(`shell:${trigger.name}: trigger=${JSON.stringify(trigger)}`); + logger.debug( + `shell:${trigger.name}: opts=${JSON.stringify(opts)}, data=${JSON.stringify(data)}`, + ); - if (!result) { - throw new FirebaseError(`Could not find trigger ${name}`); + const eventTrigger = trigger.eventTrigger; + if (!eventTrigger) { + throw new FirebaseError(`Function ${trigger.name} is not a background function`); } - return result; + let body; + if (trigger.platform === "gcfv1") { + body = this.createLegacyEvent(eventTrigger, data, opts); + } else { + body = this.createCloudEvent(eventTrigger, data, opts); + } + this.emu.sendRequest(trigger, body); } } diff --git a/src/emulator/functionsEmulatorUtils.spec.ts b/src/emulator/functionsEmulatorUtils.spec.ts new file mode 100644 index 00000000000..bb9270eee9e --- /dev/null +++ b/src/emulator/functionsEmulatorUtils.spec.ts @@ -0,0 +1,186 @@ +import { expect } from "chai"; +import { + extractParamsFromPath, + isValidWildcardMatch, + trimSlashes, + compareVersionStrings, + parseRuntimeVersion, + isLocalHost, +} from "./functionsEmulatorUtils"; + +describe("FunctionsEmulatorUtils", () => { + describe("extractParamsFromPath", () => { + it("should match a path which fits a wildcard template", () => { + const params = extractParamsFromPath( + "companies/{company}/users/{user}", + "/companies/firebase/users/abe", + ); + expect(params).to.deep.equal({ company: "firebase", user: "abe" }); + }); + + it("should not match unfilled wildcards", () => { + const params = extractParamsFromPath( + "companies/{company}/users/{user}", + "companies/{still_wild}/users/abe", + ); + expect(params).to.deep.equal({ user: "abe" }); + }); + + it("should not match a path which is too long", () => { + const params = extractParamsFromPath( + "companies/{company}/users/{user}", + "companies/firebase/users/abe/boots", + ); + expect(params).to.deep.equal({}); + }); + + it("should not match a path which is too short", () => { + const params = extractParamsFromPath( + "companies/{company}/users/{user}", + "companies/firebase/users/", + ); + expect(params).to.deep.equal({}); + }); + + it("should not match a path which has different chunks", () => { + const params = extractParamsFromPath( + "locations/{company}/users/{user}", + "companies/firebase/users/{user}", + ); + expect(params).to.deep.equal({}); + }); + }); + + describe("isValidWildcardMatch", () => { + it("should match a path which fits a wildcard template", () => { + const valid = isValidWildcardMatch( + "companies/{company}/users/{user}", + "/companies/firebase/users/abe", + ); + expect(valid).to.equal(true); + }); + + it("should not match a path which is too long", () => { + const tooLong = isValidWildcardMatch( + "companies/{company}/users/{user}", + "companies/firebase/users/abe/boots", + ); + expect(tooLong).to.equal(false); + }); + + it("should not match a path which is too short", () => { + const tooShort = isValidWildcardMatch( + "companies/{company}/users/{user}", + "companies/firebase/users/", + ); + expect(tooShort).to.equal(false); + }); + + it("should not match a path which has different chunks", () => { + const differentChunk = isValidWildcardMatch( + "locations/{company}/users/{user}", + "companies/firebase/users/{user}", + ); + expect(differentChunk).to.equal(false); + }); + }); + + describe("trimSlashes", () => { + it("should remove leading and trailing slashes", () => { + expect(trimSlashes("///a/b/c////")).to.equal("a/b/c"); + }); + it("should replace multiple adjacent slashes with a single slash", () => { + expect(trimSlashes("a////b//c")).to.equal("a/b/c"); + }); + it("should do both", () => { + expect(trimSlashes("///a////b//c/")).to.equal("a/b/c"); + }); + }); + + describe("compareVersonStrings", () => { + it("should detect a higher major version", () => { + expect(compareVersionStrings("4.0.0", "3.2.1")).to.be.gt(0); + expect(compareVersionStrings("3.2.1", "4.0.0")).to.be.lt(0); + }); + + it("should detect a higher minor version", () => { + expect(compareVersionStrings("4.1.0", "4.0.1")).to.be.gt(0); + expect(compareVersionStrings("4.0.1", "4.1.0")).to.be.lt(0); + }); + + it("should detect a higher patch version", () => { + expect(compareVersionStrings("4.0.1", "4.0.0")).to.be.gt(0); + expect(compareVersionStrings("4.0.0", "4.0.1")).to.be.lt(0); + }); + + it("should detect the same version", () => { + expect(compareVersionStrings("4.0.0", "4.0.0")).to.eql(0); + expect(compareVersionStrings("4.0", "4.0.0")).to.eql(0); + expect(compareVersionStrings("4", "4.0.0")).to.eql(0); + }); + }); + + describe("parseRuntimeVerson", () => { + it("should parse fully specified runtime strings", () => { + expect(parseRuntimeVersion("nodejs6")).to.eql(6); + expect(parseRuntimeVersion("nodejs8")).to.eql(8); + expect(parseRuntimeVersion("nodejs10")).to.eql(10); + expect(parseRuntimeVersion("nodejs12")).to.eql(12); + }); + + it("should parse plain number strings", () => { + expect(parseRuntimeVersion("6")).to.eql(6); + expect(parseRuntimeVersion("8")).to.eql(8); + expect(parseRuntimeVersion("10")).to.eql(10); + expect(parseRuntimeVersion("12")).to.eql(12); + }); + + it("should ignore unknown", () => { + expect(parseRuntimeVersion("banana")).to.eql(undefined); + }); + }); + + describe("isLocalHost", () => { + const testCases: { + desc: string; + href: string; + expected: boolean; + }[] = [ + { + desc: "should return true for localhost", + href: "http://localhost:4000", + expected: true, + }, + { + desc: "should return true for 127.0.0.1", + href: "127.0.0.1:5001/firestore", + expected: true, + }, + { + desc: "should return true for ipv6 loopback", + href: "[::1]:5001/firestore", + expected: true, + }, + { + desc: "should work with https", + href: "https://127.0.0.1:5001/firestore", + expected: true, + }, + { + desc: "should return false for external uri", + href: "http://google.com/what-is-localhost", + expected: false, + }, + { + desc: "should return false for external ip", + href: "123:100:99:12", + expected: false, + }, + ]; + for (const t of testCases) { + it(t.desc, () => { + expect(isLocalHost(t.href)).to.eq(t.expected); + }); + } + }); +}); diff --git a/src/emulator/functionsEmulatorUtils.ts b/src/emulator/functionsEmulatorUtils.ts index fcf7768488d..5a76b255606 100644 --- a/src/emulator/functionsEmulatorUtils.ts +++ b/src/emulator/functionsEmulatorUtils.ts @@ -17,7 +17,7 @@ export interface ModuleVersion { export function extractParamsFromPath( wildcardPath: string, - snapshotPath: string + snapshotPath: string, ): { [key: string]: string } { if (!isValidWildcardMatch(wildcardPath, snapshotPath)) { return {}; @@ -80,7 +80,7 @@ export function parseRuntimeVersion(runtime?: string): number | undefined { } const runtimeRe = /(nodejs)?([0-9]+)/; - const match = runtime.match(runtimeRe); + const match = runtimeRe.exec(runtime); if (match) { return Number.parseInt(match[2]); } @@ -117,17 +117,24 @@ export function compareVersionStrings(a?: string, b?: string) { const versionA = parseVersionString(a); const versionB = parseVersionString(b); - if (versionA.major != versionB.major) { + if (versionA.major !== versionB.major) { return versionA.major - versionB.major; } - if (versionA.minor != versionB.minor) { + if (versionA.minor !== versionB.minor) { return versionA.minor - versionB.minor; } - if (versionA.patch != versionB.patch) { + if (versionA.patch !== versionB.patch) { return versionA.patch - versionB.patch; } return 0; } + +/** + * Check if a url is localhost + */ +export function isLocalHost(href: string): boolean { + return !!href.match(/^(http(s)?:\/\/)?(localhost|127.0.0.1|\[::1])/); +} diff --git a/src/emulator/functionsRuntimeWorker.spec.ts b/src/emulator/functionsRuntimeWorker.spec.ts new file mode 100644 index 00000000000..ec7347778a9 --- /dev/null +++ b/src/emulator/functionsRuntimeWorker.spec.ts @@ -0,0 +1,292 @@ +import * as httpMocks from "node-mocks-http"; +import * as nock from "nock"; +import { expect } from "chai"; +import { FunctionsRuntimeInstance, IPCConn } from "./functionsEmulator"; +import { EventEmitter } from "events"; +import { RuntimeWorker, RuntimeWorkerPool, RuntimeWorkerState } from "./functionsRuntimeWorker"; +import { EmulatedTriggerDefinition } from "./functionsEmulatorShared"; +import { EmulatorLog, FunctionsExecutionMode } from "./types"; +import { ChildProcess } from "child_process"; + +/** + * Fake runtime instance we can use to simulate different subprocess conditions. + * It automatically fails or succeeds 10ms after being given work to do. + */ +class MockRuntimeInstance implements FunctionsRuntimeInstance { + process: ChildProcess; + metadata: { [key: string]: any } = {}; + events: EventEmitter = new EventEmitter(); + exit: Promise; + cwd = "/home/users/dir"; + conn = new IPCConn("/path/to/socket/foo.sock"); + + constructor() { + this.exit = new Promise((resolve) => { + this.events.on("exit", resolve); + }); + this.process = new EventEmitter() as ChildProcess; + this.process.kill = () => { + this.events.emit("log", new EmulatorLog("SYSTEM", "runtime-status", "killed")); + this.process.emit("exit"); + return true; + }; + } +} + +/** + * Test helper to count worker state transitions. + */ +class WorkerStateCounter { + counts: { [state in RuntimeWorkerState]: number } = { + CREATED: 0, + IDLE: 0, + BUSY: 0, + FINISHING: 0, + FINISHED: 0, + }; + + constructor(worker: RuntimeWorker) { + this.increment(worker.state); + worker.stateEvents.on(RuntimeWorkerState.CREATED, () => { + this.increment(RuntimeWorkerState.CREATED); + }); + worker.stateEvents.on(RuntimeWorkerState.IDLE, () => { + this.increment(RuntimeWorkerState.IDLE); + }); + worker.stateEvents.on(RuntimeWorkerState.BUSY, () => { + this.increment(RuntimeWorkerState.BUSY); + }); + worker.stateEvents.on(RuntimeWorkerState.FINISHING, () => { + this.increment(RuntimeWorkerState.FINISHING); + }); + worker.stateEvents.on(RuntimeWorkerState.FINISHED, () => { + this.increment(RuntimeWorkerState.FINISHED); + }); + } + + private increment(state: RuntimeWorkerState) { + this.counts[state]++; + } + + get total() { + return ( + this.counts.CREATED + + this.counts.IDLE + + this.counts.BUSY + + this.counts.FINISHING + + this.counts.FINISHED + ); + } +} + +function mockTrigger(id: string): EmulatedTriggerDefinition { + return { + id, + name: id, + entryPoint: id, + region: "us-central1", + platform: "gcfv2", + }; +} + +describe("FunctionsRuntimeWorker", () => { + describe("RuntimeWorker", () => { + it("goes from created --> idle --> busy --> idle in normal operation", async () => { + const scope = nock("http://localhost").get("/").reply(200); + + const worker = new RuntimeWorker("trigger", new MockRuntimeInstance(), {}); + const counter = new WorkerStateCounter(worker); + + worker.readyForWork(); + await worker.request( + { method: "GET", path: "/" }, + httpMocks.createResponse({ eventEmitter: EventEmitter }), + ); + scope.done(); + + expect(counter.counts.CREATED).to.eql(1); + expect(counter.counts.BUSY).to.eql(1); + expect(counter.counts.IDLE).to.eql(2); + expect(counter.total).to.eql(4); + }); + + it("goes from created --> idle --> busy --> finished when there's an error", async () => { + const scope = nock("http://localhost").get("/").replyWithError("boom"); + + const worker = new RuntimeWorker("trigger", new MockRuntimeInstance(), {}); + const counter = new WorkerStateCounter(worker); + + worker.readyForWork(); + await worker.request( + { method: "GET", path: "/" }, + httpMocks.createResponse({ eventEmitter: EventEmitter }), + ); + scope.done(); + + expect(counter.counts.CREATED).to.eql(1); + expect(counter.counts.IDLE).to.eql(1); + expect(counter.counts.BUSY).to.eql(1); + expect(counter.counts.FINISHED).to.eql(1); + expect(counter.total).to.eql(4); + }); + + it("goes from created --> busy --> finishing --> finished when marked", async () => { + const scope = nock("http://localhost").get("/").replyWithError("boom"); + + const worker = new RuntimeWorker("trigger", new MockRuntimeInstance(), {}); + const counter = new WorkerStateCounter(worker); + + worker.readyForWork(); + const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); + resp.on("end", () => { + worker.state = RuntimeWorkerState.FINISHING; + }); + await worker.request({ method: "GET", path: "/" }, resp); + scope.done(); + + expect(counter.counts.CREATED).to.eql(1); + expect(counter.counts.IDLE).to.eql(1); + expect(counter.counts.BUSY).to.eql(1); + expect(counter.counts.FINISHING).to.eql(1); + expect(counter.counts.FINISHED).to.eql(1); + expect(counter.total).to.eql(5); + }); + }); + + describe("RuntimeWorkerPool", () => { + it("properly manages a single worker", async () => { + const scope = nock("http://localhost").get("/").reply(200); + + const pool = new RuntimeWorkerPool(); + const triggerId = "region-trigger1"; + + // No idle workers to begin + expect(pool.getIdleWorker(triggerId)).to.be.undefined; + + // Add a worker and make sure it's there + const worker = pool.addWorker(mockTrigger(triggerId), new MockRuntimeInstance(), {}); + worker.readyForWork(); + const triggerWorkers = pool.getTriggerWorkers(triggerId); + expect(triggerWorkers.length).length.to.eq(1); + expect(pool.getIdleWorker(triggerId)).to.eql(worker); + + const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); + resp.on("end", () => { + // Finished sending response. About to go back to IDLE state. + expect(pool.getIdleWorker(triggerId)).to.be.undefined; + }); + await worker.request({ method: "GET", path: "/" }, resp); + scope.done(); + + // Completed handling request. Worker should be IDLE again. + expect(pool.getIdleWorker(triggerId)).to.eql(worker); + }); + + it("does not consider failed workers idle", async () => { + const pool = new RuntimeWorkerPool(); + const triggerId = "trigger1"; + + // No idle workers to begin + expect(pool.getIdleWorker(triggerId)).to.be.undefined; + + // Add a worker to the pool that's destined to fail. + const scope = nock("http://localhost").get("/").replyWithError("boom"); + const worker = pool.addWorker(mockTrigger(triggerId), new MockRuntimeInstance(), {}); + worker.readyForWork(); + expect(pool.getIdleWorker(triggerId)).to.eql(worker); + + // Send request to the worker. Request should fail, killing the worker. + await worker.request( + { method: "GET", path: "/" }, + httpMocks.createResponse({ eventEmitter: EventEmitter }), + ); + scope.done(); + + // Confirm there are no idle workers. + expect(pool.getIdleWorker(triggerId)).to.be.undefined; + }); + + it("exit() kills idle and busy workers", async () => { + const pool = new RuntimeWorkerPool(); + const triggerId = "trigger1"; + + const busyWorker = pool.addWorker(mockTrigger(triggerId), new MockRuntimeInstance(), {}); + busyWorker.readyForWork(); + const busyWorkerCounter = new WorkerStateCounter(busyWorker); + + const idleWorker = pool.addWorker(mockTrigger(triggerId), new MockRuntimeInstance(), {}); + idleWorker.readyForWork(); + const idleWorkerCounter = new WorkerStateCounter(idleWorker); + + // Add a worker to the pool that's destined to fail. + const scope = nock("http://localhost").get("/").reply(200); + const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); + resp.on("end", () => { + pool.exit(); + }); + await busyWorker.request({ method: "GET", path: "/" }, resp); + scope.done(); + + expect(busyWorkerCounter.counts.IDLE).to.eql(1); + expect(busyWorkerCounter.counts.BUSY).to.eql(1); + expect(busyWorkerCounter.counts.FINISHED).to.eql(1); + expect(busyWorkerCounter.total).to.eql(3); + + expect(idleWorkerCounter.counts.IDLE).to.eql(1); + expect(idleWorkerCounter.counts.FINISHED).to.eql(1); + expect(idleWorkerCounter.total).to.eql(2); + }); + + it("refresh() kills idle workers and marks busy ones as finishing", async () => { + const pool = new RuntimeWorkerPool(); + const triggerId = "trigger1"; + + const busyWorker = pool.addWorker(mockTrigger(triggerId), new MockRuntimeInstance(), {}); + busyWorker.readyForWork(); + const busyWorkerCounter = new WorkerStateCounter(busyWorker); + + const idleWorker = pool.addWorker(mockTrigger(triggerId), new MockRuntimeInstance(), {}); + idleWorker.readyForWork(); + const idleWorkerCounter = new WorkerStateCounter(idleWorker); + + // Add a worker to the pool that's destined to fail. + const scope = nock("http://localhost").get("/").reply(200); + const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); + resp.on("end", () => { + pool.refresh(); + }); + await busyWorker.request({ method: "GET", path: "/" }, resp); + scope.done(); + + expect(busyWorkerCounter.counts.BUSY).to.eql(1); + expect(busyWorkerCounter.counts.FINISHING).to.eql(1); + expect(busyWorkerCounter.counts.FINISHED).to.eql(1); + + expect(idleWorkerCounter.counts.IDLE).to.eql(1); + expect(idleWorkerCounter.counts.FINISHING).to.eql(1); + expect(idleWorkerCounter.counts.FINISHED).to.eql(1); + }); + + it("gives assigns all triggers to the same worker in sequential mode", async () => { + const scope = nock("http://localhost").get("/").reply(200); + + const triggerId1 = "region-abc"; + const triggerId2 = "region-def"; + + const pool = new RuntimeWorkerPool(FunctionsExecutionMode.SEQUENTIAL); + const worker = pool.addWorker(mockTrigger(triggerId1), new MockRuntimeInstance(), {}); + worker.readyForWork(); + + const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); + resp.on("end", () => { + expect(pool.readyForWork(triggerId1)).to.be.false; + expect(pool.readyForWork(triggerId2)).to.be.false; + }); + await worker.request({ method: "GET", path: "/" }, resp); + scope.done(); + + expect(pool.readyForWork(triggerId1)).to.be.true; + expect(pool.readyForWork(triggerId2)).to.be.true; + }); + }); +}); diff --git a/src/emulator/functionsRuntimeWorker.ts b/src/emulator/functionsRuntimeWorker.ts index 10c2445b805..d8b30bf8e87 100644 --- a/src/emulator/functionsRuntimeWorker.ts +++ b/src/emulator/functionsRuntimeWorker.ts @@ -1,18 +1,20 @@ +import * as http from "http"; import * as uuid from "uuid"; -import { FunctionsRuntimeInstance, InvokeRuntimeOpts } from "./functionsEmulator"; + +import { FunctionsRuntimeInstance } from "./functionsEmulator"; import { EmulatorLog, Emulators, FunctionsExecutionMode } from "./types"; -import { - FunctionsRuntimeArgs, - FunctionsRuntimeBundle, - getTemporarySocketPath, -} from "./functionsEmulatorShared"; +import { EmulatedTriggerDefinition, FunctionsRuntimeBundle } from "./functionsEmulatorShared"; import { EventEmitter } from "events"; -import { EmulatorLogger } from "./emulatorLogger"; +import { EmulatorLogger, ExtensionLogInfo } from "./emulatorLogger"; import { FirebaseError } from "../error"; +import { Serializable } from "child_process"; type LogListener = (el: EmulatorLog) => any; export enum RuntimeWorkerState { + // Worker has been created but is not ready to accept work + CREATED = "CREATED", + // Worker is ready to accept new work IDLE = "IDLE", @@ -27,56 +29,183 @@ export enum RuntimeWorkerState { FINISHED = "FINISHED", } +/** + * Given no trigger key, worker is given this special key. + * + * This is useful when running the Functions Emulator in debug mode + * where single process shared amongst all triggers. + */ +const FREE_WORKER_KEY = "~free~"; + export class RuntimeWorker { readonly id: string; - readonly key: string; - readonly runtime: FunctionsRuntimeInstance; + readonly triggerKey: string; - lastArgs?: FunctionsRuntimeArgs; stateEvents: EventEmitter = new EventEmitter(); - private socketReady?: Promise; private logListeners: Array = []; - private _state: RuntimeWorkerState = RuntimeWorkerState.IDLE; + private logger: EmulatorLogger; + private _state: RuntimeWorkerState = RuntimeWorkerState.CREATED; - constructor(key: string, runtime: FunctionsRuntimeInstance) { + constructor( + triggerId: string | undefined, + readonly runtime: FunctionsRuntimeInstance, + readonly extensionLogInfo: ExtensionLogInfo, + readonly timeoutSeconds?: number, + ) { this.id = uuid.v4(); - this.key = key; + this.triggerKey = triggerId || FREE_WORKER_KEY; this.runtime = runtime; - this.runtime.events.on("log", (log: EmulatorLog) => { - if (log.type === "runtime-status") { - if (log.data.state === "idle") { - if (this.state === RuntimeWorkerState.BUSY) { - this.state = RuntimeWorkerState.IDLE; - } else if (this.state === RuntimeWorkerState.FINISHING) { - this.log(`IDLE --> FINISHING`); - this.runtime.shutdown(); - } - } - } + const childProc = this.runtime.process; + let msgBuffer = ""; + childProc.on("message", (msg) => { + msgBuffer = this.processStream(msg, msgBuffer); }); - this.runtime.exit.then(() => { - this.log("exited"); + let stdBuffer = ""; + if (childProc.stdout) { + childProc.stdout.on("data", (data) => { + stdBuffer = this.processStream(data, stdBuffer); + }); + } + + if (childProc.stderr) { + childProc.stderr.on("data", (data) => { + stdBuffer = this.processStream(data, stdBuffer); + }); + } + + this.logger = triggerId + ? EmulatorLogger.forFunction(triggerId, extensionLogInfo) + : EmulatorLogger.forEmulator(Emulators.FUNCTIONS); + this.onLogs((log: EmulatorLog) => { + this.logger.handleRuntimeLog(log); + }, true /* listen forever */); + + childProc.on("exit", () => { + this.logDebug("exited"); this.state = RuntimeWorkerState.FINISHED; }); } - execute(frb: FunctionsRuntimeBundle, opts?: InvokeRuntimeOpts): void { - // Make a copy so we don't edit it - const execFrb: FunctionsRuntimeBundle = { ...frb }; + private processStream(s: Serializable, buf: string): string { + buf += s.toString(); + + const lines = buf.split("\n"); + if (lines.length > 1) { + // slice(0, -1) returns all elements but the last + lines.slice(0, -1).forEach((line: string) => { + const log = EmulatorLog.fromJSON(line); + this.runtime.events.emit("log", log); - // TODO(samstern): I would like to do this elsewhere... - if (!execFrb.socketPath) { - execFrb.socketPath = getTemporarySocketPath(this.runtime.pid, execFrb.cwd); - this.log(`Assigning socketPath: ${execFrb.socketPath}`); + if (log.level === "FATAL") { + // Something went wrong, if we don't kill the process it'll wait for timeoutMs. + this.runtime.events.emit("log", new EmulatorLog("SYSTEM", "runtime-status", "killed")); + this.runtime.process.kill(); + } + }); } + return lines[lines.length - 1]; + } + + readyForWork(): void { + this.state = RuntimeWorkerState.IDLE; + } + + sendDebugMsg(debug: FunctionsRuntimeBundle["debug"]): Promise { + return new Promise((resolve, reject) => { + this.runtime.process.send(JSON.stringify(debug), (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + request( + req: http.RequestOptions, + resp: http.ServerResponse, + body?: unknown, + debug?: boolean, + ): Promise { + if (this.triggerKey !== FREE_WORKER_KEY) { + this.logInfo(`Beginning execution of "${this.triggerKey}"`); + } + const startHrTime = process.hrtime(); - const args: FunctionsRuntimeArgs = { frb: execFrb, opts }; this.state = RuntimeWorkerState.BUSY; - this.lastArgs = args; - this.runtime.send(args); + const onFinish = (): void => { + if (this.triggerKey !== FREE_WORKER_KEY) { + const elapsedHrTime = process.hrtime(startHrTime); + this.logInfo( + `Finished "${this.triggerKey}" in ${ + elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1000000 + }ms`, + ); + } + + if (this.state === RuntimeWorkerState.BUSY) { + this.state = RuntimeWorkerState.IDLE; + } else if (this.state === RuntimeWorkerState.FINISHING) { + this.logDebug(`IDLE --> FINISHING`); + this.runtime.process.kill(); + } + }; + return new Promise((resolve) => { + const reqOpts = { + ...this.runtime.conn.httpReqOpts(), + method: req.method, + path: req.path, + headers: req.headers, + }; + if (this.timeoutSeconds) { + reqOpts.timeout = this.timeoutSeconds * 1000; + } + const proxy = http.request(reqOpts, (_resp: http.IncomingMessage) => { + resp.writeHead(_resp.statusCode || 200, _resp.headers); + + let finished = false; + const finishReq = (event?: string): void => { + this.logger.log("DEBUG", `Finishing up request with event=${event}`); + if (!finished) { + finished = true; + onFinish(); + resolve(); + } + }; + _resp.on("pause", () => finishReq("pause")); + _resp.on("close", () => finishReq("close")); + const piped = _resp.pipe(resp); + piped.on("finish", () => finishReq("finish")); + }); + if (debug) { + proxy.setSocketKeepAlive(false); + proxy.setTimeout(0); + } + proxy.on("timeout", () => { + this.logger.log( + "ERROR", + `Your function timed out after ~${this.timeoutSeconds}s. To configure this timeout, see + https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation.`, + ); + proxy.destroy(); + }); + proxy.on("error", (err) => { + this.logger.log("ERROR", `Request to function failed: ${err}`); + resp.writeHead(500); + resp.write(JSON.stringify(err)); + resp.end(); + this.runtime.process.kill(); + resolve(); + }); + if (body) { + proxy.write(body); + } + proxy.end(); + }); } get state(): RuntimeWorkerState { @@ -84,31 +213,19 @@ export class RuntimeWorker { } set state(state: RuntimeWorkerState) { - if (state === RuntimeWorkerState.BUSY) { - this.socketReady = EmulatorLog.waitForLog( - this.runtime.events, - "SYSTEM", - "runtime-status", - (el) => { - return el.data.state === "ready"; - } - ); - } - if (state === RuntimeWorkerState.IDLE) { // Remove all temporary log listeners every time we move to IDLE for (const l of this.logListeners) { this.runtime.events.removeListener("log", l); } this.logListeners = []; - this.socketReady = undefined; } if (state === RuntimeWorkerState.FINISHED) { this.runtime.events.removeAllListeners(); } - this.log(state); + this.logDebug(state); this._state = state; this.stateEvents.emit(this._state); } @@ -121,36 +238,55 @@ export class RuntimeWorker { this.runtime.events.on("log", listener); } - waitForDone(): Promise { - if (this.state === RuntimeWorkerState.IDLE || this.state === RuntimeWorkerState.FINISHED) { - return Promise.resolve(); - } - - return new Promise((res) => { - const listener = () => { - this.stateEvents.removeListener(RuntimeWorkerState.IDLE, listener); - this.stateEvents.removeListener(RuntimeWorkerState.FINISHED, listener); - res(); - }; + isSocketReady(): Promise { + return new Promise((resolve, reject) => { + const req = http.request( + { + ...this.runtime.conn.httpReqOpts(), + method: "GET", + path: "/__/health", + }, + () => { + // Set the worker state to IDLE for new work + this.readyForWork(); + resolve(); + }, + ); + req.end(); + req.on("error", (error) => { + reject(error); + }); + }); + } - // Finish on either IDLE or FINISHED states - this.stateEvents.once(RuntimeWorkerState.IDLE, listener); - this.stateEvents.once(RuntimeWorkerState.FINISHED, listener); + async waitForSocketReady(): Promise { + const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + const timeout = new Promise((resolve, reject) => { + setTimeout(() => { + reject(new FirebaseError("Failed to load function.")); + }, 30_000); }); + while (true) { + try { + await Promise.race([this.isSocketReady(), timeout]); + break; + } catch (err: any) { + // Allow us to wait until the server is listening. + if (["ECONNREFUSED", "ENOENT"].includes(err?.code)) { + await sleep(100); + continue; + } + throw err; + } + } } - waitForSocketReady(): Promise { - return ( - this.socketReady || - Promise.reject(new Error("Cannot call waitForSocketReady() if runtime is not BUSY")) - ); + private logDebug(msg: string): void { + this.logger.log("DEBUG", `[worker-${this.triggerKey}-${this.id}]: ${msg}`); } - private log(msg: string): void { - EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( - "DEBUG", - `[worker-${this.key}-${this.id}]: ${msg}` - ); + private logInfo(msg: string): void { + this.logger.logLabeled("BULLET", "functions", msg); } } @@ -159,7 +295,7 @@ export class RuntimeWorkerPool { constructor(private mode: FunctionsExecutionMode = FunctionsExecutionMode.AUTO) {} - getKey(triggerId: string | undefined) { + getKey(triggerId: string | undefined): string { if (this.mode === FunctionsExecutionMode.SEQUENTIAL) { return "~shared~"; } else { @@ -173,15 +309,15 @@ export class RuntimeWorkerPool { * each BUSY worker we move it to the FINISHING state so that it will * kill itself after it's done with its current task. */ - refresh() { + refresh(): void { for (const arr of this.workers.values()) { arr.forEach((w) => { if (w.state === RuntimeWorkerState.IDLE) { - this.log(`Shutting down IDLE worker (${w.key})`); + this.log(`Shutting down IDLE worker (${w.triggerKey})`); w.state = RuntimeWorkerState.FINISHING; - w.runtime.shutdown(); + w.runtime.process.kill(); } else if (w.state === RuntimeWorkerState.BUSY) { - this.log(`Marking BUSY worker to finish (${w.key})`); + this.log(`Marking BUSY worker to finish (${w.triggerKey})`); w.state = RuntimeWorkerState.FINISHING; } }); @@ -191,13 +327,13 @@ export class RuntimeWorkerPool { /** * Immediately kill all workers. */ - exit() { + exit(): void { for (const arr of this.workers.values()) { arr.forEach((w) => { if (w.state === RuntimeWorkerState.IDLE) { - w.runtime.shutdown(); + w.runtime.process.kill(); } else { - w.runtime.kill(); + w.runtime.process.kill(); } }); } @@ -214,29 +350,33 @@ export class RuntimeWorkerPool { } /** - * Submit work to be run by an idle worker for the givenn triggerId. - * Calls to this function should be guarded by readyForWork() to avoid throwing - * an exception. + * Submit request to be handled by an idle worker for the given triggerId. + * Caller should ensure that there is an idle worker to handle the request. * * @param triggerId - * @param frb - * @param opts + * @param req Request to send to the trigger. + * @param resp Response to proxy the response from the worker. + * @param body Request body. + * @param debug Debug payload to send prior to making request. */ - submitWork( - triggerId: string | undefined, - frb: FunctionsRuntimeBundle, - opts?: InvokeRuntimeOpts - ): RuntimeWorker { - this.log(`submitWork(triggerId=${triggerId})`); + async submitRequest( + triggerId: string, + req: http.RequestOptions, + resp: http.ServerResponse, + body: unknown, + debug?: FunctionsRuntimeBundle["debug"], + ): Promise { + this.log(`submitRequest(triggerId=${triggerId})`); const worker = this.getIdleWorker(triggerId); if (!worker) { throw new FirebaseError( - "Internal Error: can't call submitWork without checking for idle workers" + "Internal Error: can't call submitRequest without checking for idle workers", ); } - - worker.execute(frb, opts); - return worker; + if (debug) { + await worker.sendDebugMsg(debug); + } + return worker.request(req, resp, body, !!debug); } getIdleWorker(triggerId: string | undefined): RuntimeWorker | undefined { @@ -256,22 +396,33 @@ export class RuntimeWorkerPool { return; } - addWorker(triggerId: string | undefined, runtime: FunctionsRuntimeInstance): RuntimeWorker { - const worker = new RuntimeWorker(this.getKey(triggerId), runtime); - this.log(`addWorker(${worker.key})`); + /** + * Adds a worker to the pool. + * Caller must set the worker status to ready by calling + * `worker.readyForWork()` or `worker.waitForSocketReady()`. + */ + addWorker( + trigger: EmulatedTriggerDefinition | undefined, + runtime: FunctionsRuntimeInstance, + extensionLogInfo: ExtensionLogInfo, + ): RuntimeWorker { + this.log(`addWorker(${this.getKey(trigger?.id)})`); + // Disable worker timeout if: + // (1) This is a diagnostic call without trigger id OR + // (2) If in SEQUENTIAL execution mode + const disableTimeout = !trigger?.id || this.mode === FunctionsExecutionMode.SEQUENTIAL; + const worker = new RuntimeWorker( + trigger?.id, + runtime, + extensionLogInfo, + disableTimeout ? undefined : trigger?.timeoutSeconds, + ); - const keyWorkers = this.getTriggerWorkers(triggerId); + const keyWorkers = this.getTriggerWorkers(trigger?.id); keyWorkers.push(worker); - this.setTriggerWorkers(triggerId, keyWorkers); - - const logger = triggerId - ? EmulatorLogger.forFunction(triggerId) - : EmulatorLogger.forEmulator(Emulators.FUNCTIONS); - worker.onLogs((log: EmulatorLog) => { - logger.handleRuntimeLog(log); - }, true /* listen forever */); + this.setTriggerWorkers(trigger?.id, keyWorkers); - this.log(`Adding worker with key ${worker.key}, total=${keyWorkers.length}`); + this.log(`Adding worker with key ${worker.triggerKey}, total=${keyWorkers.length}`); return worker; } @@ -292,7 +443,7 @@ export class RuntimeWorkerPool { if (notDoneWorkers.length !== keyWorkers.length) { this.log( - `Cleaned up workers for ${key}: ${keyWorkers.length} --> ${notDoneWorkers.length}` + `Cleaned up workers for ${key}: ${keyWorkers.length} --> ${notDoneWorkers.length}`, ); } this.setTriggerWorkers(key, notDoneWorkers); diff --git a/src/emulator/hostingEmulator.ts b/src/emulator/hostingEmulator.ts index 5c8deb031ff..ab041ed1f16 100644 --- a/src/emulator/hostingEmulator.ts +++ b/src/emulator/hostingEmulator.ts @@ -1,4 +1,4 @@ -import serveHosting = require("../serve/hosting"); +import * as serveHosting from "../serve/hosting"; import { EmulatorInfo, EmulatorInstance, Emulators } from "../emulator/types"; import { Constants } from "./constants"; @@ -9,13 +9,19 @@ interface HostingEmulatorArgs { } export class HostingEmulator implements EmulatorInstance { + private reservedPorts?: number[]; + constructor(private args: HostingEmulatorArgs) {} - start(): Promise { + async start(): Promise { this.args.options.host = this.args.host; this.args.options.port = this.args.port; - return serveHosting.start(this.args.options); + const { ports } = await serveHosting.start(this.args.options); + this.args.port = ports[0]; + if (ports.length > 1) { + this.reservedPorts = ports.slice(1); + } } connect(): Promise { @@ -27,13 +33,14 @@ export class HostingEmulator implements EmulatorInstance { } getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.HOSTING); + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.HOSTING); return { name: this.getName(), host, port, + reservedPorts: this.reservedPorts, }; } diff --git a/src/emulator/hub.ts b/src/emulator/hub.ts index 47c3bf2ab1f..45d70286b03 100644 --- a/src/emulator/hub.ts +++ b/src/emulator/hub.ts @@ -1,36 +1,35 @@ -import * as cors from "cors"; import * as express from "express"; import * as os from "os"; import * as fs from "fs"; import * as path from "path"; -import * as bodyParser from "body-parser"; import * as utils from "../utils"; import { logger } from "../logger"; -import { Constants } from "./constants"; -import { Emulators, EmulatorInstance, EmulatorInfo } from "./types"; +import { Emulators, EmulatorInfo, ListenSpec } from "./types"; import { HubExport } from "./hubExport"; import { EmulatorRegistry } from "./registry"; import { FunctionsEmulator } from "./functionsEmulator"; +import { ExpressBasedEmulator } from "./ExpressBasedEmulator"; +import { PortName } from "./portUtils"; // We use the CLI version from package.json const pkg = require("../../package.json"); export interface Locator { version: string; - host: string; - port: number; + // Ways of reaching the hub as URL prefix, such as http://127.0.0.1:4000 + origins: string[]; } export interface EmulatorHubArgs { projectId: string; - port?: number; - host?: string; + listen: ListenSpec[]; + listenForEmulator: Record; } export type GetEmulatorsResponse = Record; -export class EmulatorHub implements EmulatorInstance { +export class EmulatorHub extends ExpressBasedEmulator { static CLI_VERSION = pkg.version; static PATH_EXPORT = "/_admin/export"; static PATH_DISABLE_FUNCTIONS = "/functions/disableBackgroundTriggers"; @@ -65,41 +64,58 @@ export class EmulatorHub implements EmulatorInstance { return path.join(dir, filename); } - private hub: express.Express; - private destroyServer?: () => Promise; - constructor(private args: EmulatorHubArgs) { - this.hub = express(); - // Enable CORS for all APIs, all origins (reflected), and all headers (reflected). - // Safe since all Hub APIs are cookieless. - this.hub.use(cors({ origin: true })); - this.hub.use(bodyParser.json()); - - this.hub.get("/", (req, res) => { - res.json(this.getLocator()); + super({ + listen: args.listen, }); + } - this.hub.get(EmulatorHub.PATH_EMULATORS, (req, res) => { - const body: GetEmulatorsResponse = {}; - EmulatorRegistry.listRunning().forEach((name) => { - body[name] = EmulatorRegistry.get(name)!.getInfo(); + override async start(): Promise { + await super.start(); + await this.writeLocatorFile(); + } + + protected override async createExpressApp(): Promise { + const app = await super.createExpressApp(); + app.get("/", (req, res) => { + res.json({ + ...this.getLocator(), + // For backward compatibility: + host: utils.connectableHostname(this.args.listen[0].address), + port: this.args.listen[0].port, }); + }); + + app.get(EmulatorHub.PATH_EMULATORS, (req, res) => { + const body: GetEmulatorsResponse = {}; + for (const info of EmulatorRegistry.listRunningWithInfo()) { + body[info.name] = { + listen: this.args.listenForEmulator[info.name], + ...info, + }; + } res.json(body); }); - this.hub.post(EmulatorHub.PATH_EXPORT, async (req, res) => { - const exportPath = req.body.path; - utils.logLabeledBullet( - "emulators", - `Received export request. Exporting data to ${exportPath}.` - ); + app.post(EmulatorHub.PATH_EXPORT, async (req, res) => { + if (req.headers.origin) { + res.status(403).json({ + message: `Export cannot be triggered by external callers.`, + }); + } + const path: string = req.body.path; + const initiatedBy: string = req.body.initiatedBy || "unknown"; + utils.logLabeledBullet("emulators", `Received export request. Exporting data to ${path}.`); try { - await new HubExport(this.args.projectId, exportPath).exportAll(); + await new HubExport(this.args.projectId, { + path, + initiatedBy, + }).exportAll(); utils.logLabeledSuccess("emulators", "Export complete."); res.status(200).send({ message: "OK", }); - } catch (e) { + } catch (e: any) { const errorString = e.message || JSON.stringify(e); utils.logLabeledWarning("emulators", `Export failed: ${errorString}`); res.status(500).json({ @@ -108,10 +124,10 @@ export class EmulatorHub implements EmulatorInstance { } }); - this.hub.put(EmulatorHub.PATH_DISABLE_FUNCTIONS, async (req, res) => { + app.put(EmulatorHub.PATH_DISABLE_FUNCTIONS, async (req, res) => { utils.logLabeledBullet( "emulators", - `Disabling Cloud Functions triggers, non-HTTP functions will not execute.` + `Disabling Cloud Functions triggers, non-HTTP functions will not execute.`, ); const instance = EmulatorRegistry.get(Emulators.FUNCTIONS); @@ -125,10 +141,10 @@ export class EmulatorHub implements EmulatorInstance { res.status(200).json({ enabled: false }); }); - this.hub.put(EmulatorHub.PATH_ENABLE_FUNCTIONS, async (req, res) => { + app.put(EmulatorHub.PATH_ENABLE_FUNCTIONS, async (req, res) => { utils.logLabeledBullet( "emulators", - `Enabling Cloud Functions triggers, non-HTTP functions will execute.` + `Enabling Cloud Functions triggers, non-HTTP functions will execute.`, ); const instance = EmulatorRegistry.get(Emulators.FUNCTIONS); @@ -141,48 +157,32 @@ export class EmulatorHub implements EmulatorInstance { await emu.reloadTriggers(); res.status(200).json({ enabled: true }); }); - } - async start(): Promise { - const { host, port } = this.getInfo(); - const server = this.hub.listen(port, host); - this.destroyServer = utils.createDestroyer(server); - await this.writeLocatorFile(); - } - - async connect(): Promise { - // No-op + return app; } async stop(): Promise { - if (this.destroyServer) { - await this.destroyServer(); - } + await super.stop(); await this.deleteLocatorFile(); } - getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.HUB); - const port = this.args.port || Constants.getDefaultPort(Emulators.HUB); - - return { - name: this.getName(), - host, - port, - }; - } - getName(): Emulators { return Emulators.HUB; } private getLocator(): Locator { - const { host, port } = this.getInfo(); const version = pkg.version; + const origins: string[] = []; + for (const spec of this.args.listen) { + if (spec.family === "IPv6") { + origins.push(`http://[${utils.connectableHostname(spec.address)}]:${spec.port}`); + } else { + origins.push(`http://${utils.connectableHostname(spec.address)}:${spec.port}`); + } + } return { version, - host, - port, + origins, }; } @@ -194,7 +194,7 @@ export class EmulatorHub implements EmulatorInstance { if (fs.existsSync(locatorPath)) { utils.logLabeledWarning( "emulators", - `It seems that you are running multiple instances of the emulator suite for project ${projectId}. This may result in unexpected behavior.` + `It seems that you are running multiple instances of the emulator suite for project ${projectId}. This may result in unexpected behavior.`, ); } @@ -214,7 +214,8 @@ export class EmulatorHub implements EmulatorInstance { const locatorPath = EmulatorHub.getLocatorFilePath(this.args.projectId); return new Promise((resolve, reject) => { fs.unlink(locatorPath, (e) => { - if (e) { + // If the file is already deleted, no need to throw. + if (e && e.code !== "ENOENT") { reject(e); } else { resolve(); diff --git a/src/emulator/hubClient.ts b/src/emulator/hubClient.ts index bba364dc75c..5c411171e29 100644 --- a/src/emulator/hubClient.ts +++ b/src/emulator/hubClient.ts @@ -1,6 +1,7 @@ -import * as api from "../api"; import { EmulatorHub, Locator, GetEmulatorsResponse } from "./hub"; import { FirebaseError } from "../error"; +import { Client } from "../apiv2"; +import { ExportOptions } from "./hubExport"; export class EmulatorHubClient { private locator: Locator | undefined; @@ -13,36 +14,45 @@ export class EmulatorHubClient { return this.locator !== undefined; } - getStatus(): Promise { - return api.request("GET", "/", { - origin: this.origin, + /** + * Ping possible hub origins for status and return the first successful. + */ + getStatus(): Promise { + return this.tryOrigins(async (client, origin) => { + await client.get("/"); + return origin; }); } - getEmulators(): Promise { - return api - .request("GET", EmulatorHub.PATH_EMULATORS, { - origin: this.origin, - json: true, - }) - .then((res) => { - return res.body as GetEmulatorsResponse; - }); + private async tryOrigins(task: (client: Client, origin: string) => Promise): Promise { + const origins = this.assertLocator().origins; + let err: any = undefined; + for (const origin of origins) { + try { + const apiClient = new Client({ urlPrefix: origin, auth: false }); + return await task(apiClient, origin); + } catch (e) { + if (!err) { + err = e; // Only record the first error and only throw if all fails. + } + } + } + throw err ?? new Error("Cannot find working hub origin. Tried:" + origins.join(" ")); } - postExport(path: string): Promise { - return api.request("POST", EmulatorHub.PATH_EXPORT, { - origin: this.origin, - json: true, - data: { - path, - }, - }); + async getEmulators(): Promise { + const res = await this.tryOrigins((client) => + client.get(EmulatorHub.PATH_EMULATORS), + ); + return res.body; } - get origin(): string { - const locator = this.assertLocator(); - return `http://${locator.host}:${locator.port}`; + async postExport(options: ExportOptions): Promise { + // This is a POST operation that should not be retried / multicast, so we + // will try to find the right origin first via GET. + const origin = await this.getStatus(); + const apiClient = new Client({ urlPrefix: origin, auth: false }); + await apiClient.post(EmulatorHub.PATH_EXPORT, options); } private assertLocator(): Locator { diff --git a/src/emulator/hubExport.ts b/src/emulator/hubExport.ts index e6e117d52f0..463a755815b 100644 --- a/src/emulator/hubExport.ts +++ b/src/emulator/hubExport.ts @@ -3,7 +3,6 @@ import * as fs from "fs"; import * as fse from "fs-extra"; import * as http from "http"; -import * as api from "../api"; import { logger } from "../logger"; import { IMPORT_EXPORT_EMULATORS, Emulators, ALL_EMULATORS } from "./types"; import { EmulatorRegistry } from "./registry"; @@ -11,8 +10,8 @@ import { FirebaseError } from "../error"; import { EmulatorHub } from "./hub"; import { getDownloadDetails } from "./downloadableEmulators"; import { DatabaseEmulator } from "./databaseEmulator"; -import { StorageEmulator } from "./storage"; import * as rimraf from "rimraf"; +import { trackEmulator } from "../track"; export interface FirestoreExportMetadata { version: string; @@ -43,12 +42,22 @@ export interface ExportMetadata { storage?: StorageExportMetadata; } +export interface ExportOptions { + path: string; + initiatedBy: string; +} + export class HubExport { static METADATA_FILE_NAME = "firebase-export-metadata.json"; private tmpDir: string; + private exportPath: string; - constructor(private projectId: string, private exportPath: string) { + constructor( + private projectId: string, + private options: ExportOptions, + ) { + this.exportPath = options.path; this.tmpDir = fs.mkdtempSync(`firebase-export-${new Date().getTime()}`); } @@ -112,6 +121,11 @@ export class HubExport { fs.mkdirSync(this.exportPath); } + void trackEmulator("emulator_export", { + initiated_by: this.options.initiatedBy, + emulator_name: Emulators.HUB, + }); + // Write the metadata file after everything else has succeeded const metadataPath = path.join(this.tmpDir, HubExport.METADATA_FILE_NAME); fs.writeFileSync(metadataPath, JSON.stringify(metadata, undefined, 2)); @@ -124,8 +138,10 @@ export class HubExport { } private async exportFirestore(metadata: ExportMetadata): Promise { - const firestoreInfo = EmulatorRegistry.get(Emulators.FIRESTORE)!.getInfo(); - const firestoreHost = `http://${EmulatorRegistry.getInfoHostString(firestoreInfo)}`; + void trackEmulator("emulator_export", { + initiated_by: this.options.initiatedBy, + emulator_name: Emulators.FIRESTORE, + }); const firestoreExportBody = { database: `projects/${this.projectId}/databases/(default)`, @@ -133,29 +149,33 @@ export class HubExport { export_name: metadata.firestore!!.path, }; - return api.request("POST", `/emulator/v1/projects/${this.projectId}:export`, { - origin: firestoreHost, - json: true, - data: firestoreExportBody, - }); + await EmulatorRegistry.client(Emulators.FIRESTORE).post( + `/emulator/v1/projects/${this.projectId}:export`, + firestoreExportBody, + ); } private async exportDatabase(metadata: ExportMetadata): Promise { const databaseEmulator = EmulatorRegistry.get(Emulators.DATABASE) as DatabaseEmulator; - const databaseAddr = `http://${EmulatorRegistry.getInfoHostString(databaseEmulator.getInfo())}`; + const client = EmulatorRegistry.client(Emulators.DATABASE, { auth: true }); // Get the list of namespaces - const inspectURL = `/.inspect/databases.json?ns=${this.projectId}`; - const inspectRes = await api.request("GET", inspectURL, { origin: databaseAddr, auth: true }); + const inspectURL = `/.inspect/databases.json`; + const inspectRes = await client.get>(inspectURL, { + queryParams: { ns: this.projectId }, + }); const namespaces = inspectRes.body.map((instance: any) => instance.name); // Check each one for actual data const namespacesToExport: string[] = []; for (const ns of namespaces) { - const checkDataPath = `/.json?ns=${ns}&shallow=true&limitToFirst=1`; - const checkDataRes = await api.request("GET", checkDataPath, { - origin: databaseAddr, - auth: true, + const checkDataPath = `/.json`; + const checkDataRes = await client.get(checkDataPath, { + queryParams: { + ns, + shallow: "true", + limitToFirst: 1, + }, }); if (checkDataRes.body !== null) { namespacesToExport.push(ns); @@ -171,6 +191,11 @@ export class HubExport { namespacesToExport.push(ns); } } + void trackEmulator("emulator_export", { + initiated_by: this.options.initiatedBy, + emulator_name: Emulators.DATABASE, + count: namespacesToExport.length, + }); const dbExportPath = path.join(this.tmpDir, metadata.database!.path); if (!fs.existsSync(dbExportPath)) { @@ -189,12 +214,16 @@ export class HubExport { path: `/.json?ns=${ns}&format=export`, headers: { Authorization: "Bearer owner" }, }, - exportFile + exportFile, ); } } private async exportAuth(metadata: ExportMetadata): Promise { + void trackEmulator("emulator_export", { + initiated_by: this.options.initiatedBy, + emulator_name: Emulators.AUTH, + }); const { host, port } = EmulatorRegistry.get(Emulators.AUTH)!.getInfo(); const authExportPath = path.join(this.tmpDir, metadata.auth!.path); @@ -213,7 +242,7 @@ export class HubExport { path: `/identitytoolkit.googleapis.com/v1/projects/${this.projectId}/accounts:batchGet?maxResults=-1`, headers: { Authorization: "Bearer owner" }, }, - accountsFile + accountsFile, ); const configFile = path.join(authExportPath, "config.json"); @@ -225,13 +254,11 @@ export class HubExport { path: `/emulator/v1/projects/${this.projectId}/config`, headers: { Authorization: "Bearer owner" }, }, - configFile + configFile, ); } private async exportStorage(metadata: ExportMetadata): Promise { - const storageEmulator = EmulatorRegistry.get(Emulators.STORAGE) as StorageEmulator; - // Clear the export const storageExportPath = path.join(this.tmpDir, metadata.storage!.path); if (fs.existsSync(storageExportPath)) { @@ -239,16 +266,22 @@ export class HubExport { } fs.mkdirSync(storageExportPath, { recursive: true }); - const storageHost = `http://${EmulatorRegistry.getInfoHostString(storageEmulator.getInfo())}`; const storageExportBody = { path: storageExportPath, + initiatedBy: this.options.initiatedBy, }; - return api.request("POST", "/internal/export", { - origin: storageHost, - json: true, - data: storageExportBody, + const res = await EmulatorRegistry.client(Emulators.STORAGE).request({ + method: "POST", + path: "/internal/export", + headers: { "Content-Type": "application/json" }, + body: storageExportBody, + responseType: "stream", + resolveOnHTTPError: true, }); + if (res.status >= 400) { + throw new FirebaseError(`Failed to export storage: ${await res.response.text()}`); + } } } diff --git a/src/emulator/loggingEmulator.ts b/src/emulator/loggingEmulator.ts index 38d1666416e..ed6035be371 100644 --- a/src/emulator/loggingEmulator.ts +++ b/src/emulator/loggingEmulator.ts @@ -5,7 +5,7 @@ import * as WebSocket from "ws"; import { LogEntry } from "winston"; import * as TransportStream from "winston-transport"; import { logger } from "../logger"; -const ansiStrip = require("cli-color/strip"); +const stripAnsi = require("strip-ansi"); export interface LoggingEmulatorArgs { port?: number; @@ -24,6 +24,10 @@ export interface LogData { function?: { name: string; }; + extension?: { + ref?: string; + instanceId?: string; + }; }; } @@ -54,7 +58,7 @@ export class LoggingEmulator implements EmulatorInstance { } getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.LOGGING); + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.LOGGING); return { @@ -115,11 +119,11 @@ class WebSocketTransport extends TransportStream { const splat = [info.message, ...(info[SPLAT] || [])] .map((value) => { - if (typeof value == "string") { + if (typeof value === "string") { try { bundle.data = { ...bundle.data, ...JSON.parse(value) }; return null; - } catch (err) { + } catch (err: any) { // If the value isn't JSONable, just treat it like a string return value; } @@ -141,7 +145,7 @@ class WebSocketTransport extends TransportStream { bundle.message = bundle.data.metadata.message; } - bundle.message = ansiStrip(bundle.message); + bundle.message = stripAnsi(bundle.message); this.history.push(bundle); this.connections.forEach((ws) => { diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index d911c054541..5485b4768a4 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -1,13 +1,21 @@ -import * as pf from "portfinder"; +import * as clc from "colorette"; import * as tcpport from "tcp-port-used"; +import * as dns from "dns"; +import { createServer } from "node:net"; import { FirebaseError } from "../error"; -import { logger } from "../logger"; +import * as utils from "../utils"; +import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED, Resolver } from "./dns"; +import { Emulators, ListenSpec } from "./types"; +import { Constants } from "./constants"; +import { EmulatorLogger } from "./emulatorLogger"; +import { execSync } from "node:child_process"; +import { checkIfDataConnectEmulatorRunningOnAddress } from "./dataconnectEmulator"; // See: // - https://stackoverflow.com/questions/4313403/why-do-browsers-block-some-ports // - https://chromium.googlesource.com/chromium/src.git/+/refs/heads/master/net/base/port_util.cc -const RESTRICTED_PORTS = [ +const RESTRICTED_PORTS = new Set([ 1, // tcpmux 7, // echo 9, // discard @@ -75,19 +83,19 @@ const RESTRICTED_PORTS = [ 6668, // Alternate IRC [Apple addition] 6669, // Alternate IRC [Apple addition] 6697, // IRC + TLS -]; +]); /** * Check if a given port is restricted by Chrome. */ -export function isRestricted(port: number): boolean { - return RESTRICTED_PORTS.includes(port); +function isRestricted(port: number): boolean { + return RESTRICTED_PORTS.has(port); } /** * Suggest a port equal to or higher than the given port which is not restricted by Chrome. */ -export function suggestUnrestricted(port: number): number { +function suggestUnrestricted(port: number): number { if (!isRestricted(port)) { return port; } @@ -101,48 +109,386 @@ export function suggestUnrestricted(port: number): number { } /** - * Find an available (unused) port on the given host. - * @param host the host. - * @param start the lowest port to search. - * @param avoidRestricted when true (default) ports which are restricted by Chrome are excluded. + * Check if a port is available for listening on the given address. */ -export async function findAvailablePort( - host: string, - start: number, - avoidRestricted = true -): Promise { - const openPort = await pf.getPortPromise({ host, port: start }); - - if (avoidRestricted && isRestricted(openPort)) { - logger.debug(`portUtils: skipping restricted port ${openPort}`); - return findAvailablePort(host, suggestUnrestricted(openPort), avoidRestricted); - } +export async function checkListenable(addr: dns.LookupAddress, port: number): Promise; +export async function checkListenable(listen: ListenSpec): Promise; +export async function checkListenable( + arg1: dns.LookupAddress | ListenSpec, + port?: number, +): Promise { + const addr = + port === undefined ? (arg1 as ListenSpec) : listenSpec(arg1 as dns.LookupAddress, port); - return openPort; + // Not using tcpport.check since it is based on trying to establish a Socket + // connection, not on *listening* on a host:port. + return new Promise((resolve, reject) => { + // For SOME REASON, we can still create a server on port 5000 on macOS. Why + // we do not know, but we need to keep this stupid check here because we + // *do* want to still *try* to default to 5000. + if (process.platform === "darwin") { + try { + execSync(`lsof -i :${addr.port} -sTCP:LISTEN`); + // If this succeeds, it found something listening. Fail. + return resolve(false); + } catch (e) { + // If lsof errored the port is NOT in use, continue. + } + } + const dummyServer = createServer(); + dummyServer.once("error", (err) => { + dummyServer.removeAllListeners(); + const e = err as Error & { code?: string }; + if (e.code === "EADDRINUSE" || e.code === "EACCES") { + resolve(false); + } else { + reject(e); + } + }); + dummyServer.once("listening", () => { + dummyServer.removeAllListeners(); + dummyServer.close((err) => { + dummyServer.removeAllListeners(); + if (err) { + reject(err); + } else { + resolve(true); + } + }); + }); + dummyServer.listen({ host: addr.address, port: addr.port, ipv6Only: addr.family === "IPv6" }); + }); } /** - * Check if a port is open on the given host. + * Wait for a port to be available on the given host. Checks every 250ms for up to timeout (default 60s). */ -export async function checkPortOpen(port: number, host: string): Promise { +export async function waitForPortUsed( + port: number, + host: string, + timeout: number = 60_000, +): Promise { + const interval = 200; try { - const inUse = await tcpport.check(port, host); - return !inUse; - } catch (e) { - logger.debug(`port check error: ${e}`); - return false; + await tcpport.waitUntilUsedOnHost(port, host, interval, timeout); + } catch (e: any) { + throw new FirebaseError(`TIMEOUT: Port ${port} on ${host} was not active within ${timeout}ms`); } } +export type PortName = Emulators | "firestore.websocket"; + +const EMULATOR_CAN_LISTEN_ON_PRIMARY_ONLY: Record = { + // External processes that accept only one hostname and one port, and will + // bind to only one of the addresses resolved from hostname. + database: true, + firestore: true, + "firestore.websocket": true, + pubsub: true, + + // External processes that accepts multiple listen specs. + dataconnect: false, + + // Listening on multiple addresses to maximize the chance of discovery. + hub: false, + + // Separate Node.js process that supports multi-listen. For consistency, we + // resolve the addresses in the CLI and pass the result to the UI. + ui: false, + + // TODO: Modify the following emulators to listen on multiple addresses. + + // Express-based servers, can be reused for multiple listen sockets. + auth: true, + eventarc: true, + extensions: true, + functions: true, + logging: true, + storage: true, + + // Only one hostname possible in .server mode, can switch to middleware later. + hosting: true, +}; + +export interface EmulatorListenConfig { + host: string; + port: number; + portFixed?: boolean; +} + +const MAX_PORT = 65535; // max TCP port + /** - * Wait for a port to close on the given host. Checks every 250ms for up to 30s. + * Resolve the hostname and assign ports to a subset of emulators. + * + * @param listenConfig the config for each emulator or previously resolved specs + * @return a map from emulator to its resolved addresses with port. */ -export async function waitForPortClosed(port: number, host: string): Promise { - const interval = 250; - const timeout = 60000; - try { - await tcpport.waitUntilUsedOnHost(port, host, interval, timeout); - } catch (e) { - throw new FirebaseError(`TIMEOUT: Port ${port} on ${host} was not active within ${timeout}ms`); +export async function resolveHostAndAssignPorts( + listenConfig: Partial>, +): Promise> { + const lookupForHost = new Map>(); + const takenPorts = new Map(); + + const result = {} as Record; + const tasks = []; + for (const name of Object.keys(listenConfig) as PortName[]) { + const config = listenConfig[name]; + if (!config) { + continue; + } else if (config instanceof Array) { + result[name] = config; + for (const { port } of config) { + takenPorts.set(port, name); + } + continue; + } + const { host, port, portFixed } = config; + let lookup = lookupForHost.get(host); + if (!lookup) { + lookup = Resolver.DEFAULT.lookupAll(host); + lookupForHost.set(host, lookup); + } + const findAddrs = lookup.then(async (addrs) => { + const emuLogger = EmulatorLogger.forEmulator( + name === "firestore.websocket" ? Emulators.FIRESTORE : name, + ); + if (addrs.some((addr) => addr.address === IPV6_UNSPECIFIED.address)) { + if (!addrs.some((addr) => addr.address === IPV4_UNSPECIFIED.address)) { + // In normal Node.js code (including CLI versions so far), listening + // on IPv6 :: will also listen on IPv4 0.0.0.0 (a.k.a. "dual stack"). + // Maintain that behavior if both are listenable. Warn otherwise. + emuLogger.logLabeled( + "DEBUG", + name, + `testing listening on IPv4 wildcard in addition to IPv6. To listen on IPv6 only, use "::0" instead.`, + ); + addrs.push(IPV4_UNSPECIFIED); + } + } + for (let p = port; p <= MAX_PORT; p++) { + if (takenPorts.has(p)) { + continue; + } + if (!portFixed && RESTRICTED_PORTS.has(p)) { + emuLogger.logLabeled("DEBUG", name, `portUtils: skipping restricted port ${p}`); + continue; + } + if (p === 5001 && /^hosting/i.exec(name)) { + // We don't want Hosting to ever try to take port 5001. + continue; + } + const available: ListenSpec[] = []; + const unavailable: string[] = []; + let i; + for (i = 0; i < addrs.length; i++) { + const addr = addrs[i]; + const listen = listenSpec(addr, p); + // This must be done one by one since the addresses may overlap. + let listenable: boolean; + try { + listenable = await checkListenable(listen); + } catch (err) { + emuLogger.logLabeled( + "WARN", + name, + `Error when trying to check port ${p} on ${addr.address}: ${err}`, + ); + // Even if portFixed is false, don't try other ports since the + // address may be entirely unavailable on all ports (e.g. no IPv6). + // https://github.com/firebase/firebase-tools/issues/4741#issuecomment-1275318134 + unavailable.push(addr.address); + continue; + } + if (listenable) { + available.push(listen); + } else { + if (/^dataconnect/i.exec(name)) { + const alreadyRunning = await checkIfDataConnectEmulatorRunningOnAddress(listen); + // If there is already a running Data Connect emulator on this address, we're gonna try to use it. + // If it's for a different service, we'll error out later from DataconnectEmulator.start(). + if (alreadyRunning) { + emuLogger.logLabeled( + "DEBUG", + "dataconnect", + `Detected already running emulator on ${listen.address}:${listen.port}. Will attempt to reuse it.`, + ); + } + available.push(listen); + continue; + } + if (!portFixed) { + // Try to find another port to avoid any potential conflict. + if (i > 0) { + emuLogger.logLabeled( + "DEBUG", + name, + `Port ${p} taken on secondary address ${addr.address}, will keep searching to find a better port.`, + ); + } + break; + } + unavailable.push(addr.address); + } + } + if (i === addrs.length) { + if (unavailable.length > 0) { + if (unavailable[0] === addrs[0].address) { + // The port is not available on the primary address, we should err + // on the side of safety and let the customer choose a different port. + return fixedPortNotAvailable(name, host, port, emuLogger, unavailable); + } + // For backward compatibility, we'll start listening as long as + // the primary address is available. Skip listening on the + // unavailable ones with a warning. + warnPartiallyAvailablePort(emuLogger, port, available, unavailable); + } + + // If available, take it and prevent any other emulator from doing so. + if (takenPorts.has(p)) { + continue; + } + takenPorts.set(p, name); + + if (RESTRICTED_PORTS.has(p)) { + const suggested = suggestUnrestricted(port); + emuLogger.logLabeled( + "WARN", + name, + `Port ${port} is restricted by some web browsers, including Chrome. You may want to choose a different port such as ${suggested}.`, + ); + } + if (p !== port && name !== "firestore.websocket") { + emuLogger.logLabeled( + "WARN", + `${portDescription(name)} unable to start on port ${port}, starting on ${p} instead.`, + ); + } + if (available.length > 1 && EMULATOR_CAN_LISTEN_ON_PRIMARY_ONLY[name]) { + emuLogger.logLabeled( + "DEBUG", + name, + `${portDescription(name)} only supports listening on one address (${ + available[0].address + }). Not listening on ${addrs + .slice(1) + .map((s) => s.address) + .join(",")}`, + ); + result[name] = [available[0]]; + } else { + result[name] = available; + } + return; + } + } + // This should be extremely rare. + return utils.reject( + `Could not find any open port in ${port}-${MAX_PORT} for ${portDescription(name)}`, + {}, + ); + }); + tasks.push(findAddrs); } + + await Promise.all(tasks); + return result; +} + +function portDescription(name: PortName): string { + return name === "firestore.websocket" + ? `websocket server for ${Emulators.FIRESTORE}` + : Constants.description(name); +} + +function warnPartiallyAvailablePort( + emuLogger: EmulatorLogger, + port: number, + available: ListenSpec[], + unavailable: string[], +): void { + emuLogger.logLabeled( + "WARN", + `Port ${port} is available on ` + + available.map((s) => s.address).join(",") + + ` but not ${unavailable.join(",")}. This may cause issues with some clients.`, + ); + emuLogger.logLabeled( + "WARN", + `If you encounter connectivity issues, consider switching to a different port or explicitly specifying ${clc.yellow( + '"host": ""', + )} instead of hostname in firebase.json`, + ); +} + +function fixedPortNotAvailable( + name: PortName, + host: string, + port: number, + emuLogger: EmulatorLogger, + unavailableAddrs: string[], +): Promise { + if (unavailableAddrs.length !== 1 || unavailableAddrs[0] !== host) { + // Show detailed resolved addresses + host = `${host} (${unavailableAddrs.join(",")})`; + } + const description = portDescription(name); + emuLogger.logLabeled( + "WARN", + `Port ${port} is not open on ${host}, could not start ${description}.`, + ); + if (name === "firestore.websocket") { + emuLogger.logLabeled( + "WARN", + `To select a different port, specify that port in a firebase.json config file: + { + // ... + "emulators": { + "${Emulators.FIRESTORE}": { + "host": "${clc.yellow("HOST")}", + ... + "websocketPort": "${clc.yellow("WEBSOCKET_PORT")}" + } + } + }`, + ); + } else { + emuLogger.logLabeled( + "WARN", + `To select a different host/port, specify that host/port in a firebase.json config file: + { + // ... + "emulators": { + "${emuLogger.name}": { + "host": "${clc.yellow("HOST")}", + "port": "${clc.yellow("PORT")}" + } + } + }`, + ); + } + return utils.reject(`Could not start ${description}, port taken.`, {}); +} + +function listenSpec(lookup: dns.LookupAddress, port: number): ListenSpec { + if (lookup.family !== 4 && lookup.family !== 6) { + throw new Error(`Unsupported address family "${lookup.family}" for address ${lookup.address}.`); + } + return { + address: lookup.address, + family: lookup.family === 4 ? "IPv4" : "IPv6", + port: port, + }; +} + +/** + * Return a comma-separated list of host:port from specs. + */ +export function listenSpecsToString(specs: ListenSpec[]): string { + return specs + .map((spec) => { + const host = spec.family === "IPv4" ? spec.address : `[${spec.address}]`; + return `${host}:${spec.port}`; + }) + .join(","); } diff --git a/src/emulator/pubsubEmulator.ts b/src/emulator/pubsubEmulator.ts index 5d86aaa30c1..94ee87baa92 100644 --- a/src/emulator/pubsubEmulator.ts +++ b/src/emulator/pubsubEmulator.ts @@ -11,6 +11,14 @@ import { FirebaseError } from "../error"; import { EmulatorRegistry } from "./registry"; import { SignatureType } from "./functionsEmulatorShared"; import { CloudEvent } from "./events/types"; +import { execSync } from "child_process"; + +// Finds processes with "pubsub-emulator" in the description and runs `kill` if any exist +// Since the pubsub emulator doesn't export any data, force-killing will not affect export-on-exit +// Note the `[p]` is a workaround to avoid selecting the currently running `ps` process. +const PUBSUB_KILL_COMMAND = + "pubsub_pids=$(ps aux | grep '[p]ubsub-emulator' | awk '{print $2}');" + + " if [ ! -z '$pubsub_pids' ]; then kill -9 $pubsub_pids; fi;"; export interface PubsubEmulatorArgs { projectId: string; @@ -25,7 +33,7 @@ interface Trigger { } export class PubsubEmulator implements EmulatorInstance { - pubsub: PubSub; + private _pubsub: PubSub | undefined; // Map of topic name to a list of functions to trigger triggersForTopic: Map; @@ -38,12 +46,17 @@ export class PubsubEmulator implements EmulatorInstance { private logger = EmulatorLogger.forEmulator(Emulators.PUBSUB); + get pubsub(): PubSub { + if (!this._pubsub) { + this._pubsub = new PubSub({ + apiEndpoint: EmulatorRegistry.url(Emulators.PUBSUB).host, + projectId: this.args.projectId, + }); + } + return this._pubsub; + } + constructor(private args: PubsubEmulatorArgs) { - const { host, port } = this.getInfo(); - this.pubsub = new PubSub({ - apiEndpoint: `${host}:${port}`, - projectId: this.args.projectId, - }); this.triggersForTopic = new Map(); this.subscriptionForTopic = new Map(); } @@ -57,11 +70,19 @@ export class PubsubEmulator implements EmulatorInstance { } async stop(): Promise { - await downloadableEmulators.stop(Emulators.PUBSUB); + try { + await downloadableEmulators.stop(Emulators.PUBSUB); + } catch (e: unknown) { + this.logger.logLabeled("DEBUG", "pubsub", JSON.stringify(e)); + if (process.platform !== "win32") { + const buffer = execSync(PUBSUB_KILL_COMMAND); + this.logger.logLabeled("DEBUG", "pubsub", "Pubsub kill output: " + JSON.stringify(buffer)); + } + } } getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.PUBSUB); + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.PUBSUB); return { @@ -76,27 +97,13 @@ export class PubsubEmulator implements EmulatorInstance { return Emulators.PUBSUB; } - async addTrigger(topicName: string, triggerKey: string, signatureType: SignatureType) { - this.logger.logLabeled( - "DEBUG", - "pubsub", - `addTrigger(${topicName}, ${triggerKey}, ${signatureType})` - ); - - const triggers = this.triggersForTopic.get(topicName) || []; - if ( - triggers.some((t) => t.triggerKey === triggerKey) && - this.subscriptionForTopic.has(topicName) - ) { - this.logger.logLabeled("DEBUG", "pubsub", "Trigger already exists"); - return; - } - + private async maybeCreateTopicAndSub(topicName: string): Promise { const topic = this.pubsub.topic(topicName); try { this.logger.logLabeled("DEBUG", "pubsub", `Creating topic: ${topicName}`); await topic.create(); - } catch (e) { + } catch (e: any) { + // CODE 6: ALREADY EXISTS. Carry on. if (e && e.code === 6) { this.logger.logLabeled("DEBUG", "pubsub", `Topic ${topicName} exists`); } else { @@ -105,14 +112,15 @@ export class PubsubEmulator implements EmulatorInstance { } const subName = `emulator-sub-${topicName}`; - let sub; + let sub: Subscription; try { this.logger.logLabeled("DEBUG", "pubsub", `Creating sub for topic: ${topicName}`); [sub] = await topic.createSubscription(subName); - } catch (e) { + } catch (e: any) { if (e && e.code === 6) { + // CODE 6: ALREADY EXISTS. Carry on. this.logger.logLabeled("DEBUG", "pubsub", `Sub for ${topicName} exists`); - sub = topic.subscription(`emulator-sub-${topicName}`); + sub = topic.subscription(subName); } else { throw new FirebaseError(`Could not create sub ${subName}`, { original: e }); } @@ -122,24 +130,41 @@ export class PubsubEmulator implements EmulatorInstance { this.onMessage(topicName, message); }); + return sub; + } + + async addTrigger(topicName: string, triggerKey: string, signatureType: SignatureType) { + this.logger.logLabeled( + "DEBUG", + "pubsub", + `addTrigger(${topicName}, ${triggerKey}, ${signatureType})`, + ); + + const sub = await this.maybeCreateTopicAndSub(topicName); + + const triggers = this.triggersForTopic.get(topicName) || []; + if ( + triggers.some((t) => t.triggerKey === triggerKey) && + this.subscriptionForTopic.has(topicName) + ) { + this.logger.logLabeled("DEBUG", "pubsub", "Trigger already exists"); + return; + } + triggers.push({ triggerKey, signatureType }); this.triggersForTopic.set(topicName, triggers); this.subscriptionForTopic.set(topicName, sub); } private ensureFunctionsClient() { - if (this.client != undefined) return; + if (this.client !== undefined) return; - const funcEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); - if (!funcEmulator) { + if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { throw new FirebaseError( - `Attempted to execute pubsub trigger but could not find the Functions emulator` + `Attempted to execute pubsub trigger but could not find the Functions emulator`, ); } - this.client = new Client({ - urlPrefix: `http://${EmulatorRegistry.getInfoHostString(funcEmulator.getInfo())}`, - auth: false, - }); + this.client = EmulatorRegistry.client(Emulators.FUNCTIONS); } private createLegacyEventRequestBody(topic: string, message: Message) { @@ -162,22 +187,31 @@ export class PubsubEmulator implements EmulatorInstance { private createCloudEventRequestBody( topic: string, - message: Message + message: Message, ): CloudEvent { + // Pubsub events from Pubsub Emulator include a date with nanoseconds. + // Prod Pubsub doesn't publish timestamp at that level of precision. Timestamp with nanosecond precision also + // are difficult to parse in languages other than Node.js (e.g. python). + const truncatedPublishTime = new Date(message.publishTime.getMilliseconds()).toISOString(); const data: MessagePublishedData = { message: { messageId: message.id, - publishTime: message.publishTime, + publishTime: truncatedPublishTime, attributes: message.attributes, orderingKey: message.orderingKey, data: message.data.toString("base64"), - }, + + // NOTE: We include camel_cased attributes since they also available and depended on by other runtimes + // like python. + message_id: message.id, + publish_time: truncatedPublishTime, + } as MessagePublishedData["message"], subscription: this.subscriptionForTopic.get(topic)!.name, }; return { - specversion: "1", + specversion: "1.0", id: uuid.v4(), - time: message.publishTime.toISOString(), + time: truncatedPublishTime, type: "google.cloud.pubsub.topic.v1.messagePublished", source: `//pubsub.googleapis.com/projects/${this.args.projectId}/topics/${topic}`, data, @@ -195,8 +229,8 @@ export class PubsubEmulator implements EmulatorInstance { "DEBUG", "pubsub", `Executing ${triggers.length} matching triggers (${JSON.stringify( - triggers.map((t) => t.triggerKey) - )})` + triggers.map((t) => t.triggerKey), + )})`, ); this.ensureFunctionsClient(); @@ -210,12 +244,12 @@ export class PubsubEmulator implements EmulatorInstance { await this.client!.post, unknown>( path, this.createCloudEventRequestBody(topicName, message), - { headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" } } + { headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" } }, ); } else { throw new FirebaseError(`Unsupported trigger signature: ${signatureType}`); } - } catch (e) { + } catch (e: any) { this.logger.logLabeled("DEBUG", "pubsub", e); } } diff --git a/src/emulator/registry.spec.ts b/src/emulator/registry.spec.ts new file mode 100644 index 00000000000..b0dbf69f0b1 --- /dev/null +++ b/src/emulator/registry.spec.ts @@ -0,0 +1,135 @@ +import { ALL_EMULATORS, Emulators } from "./types"; +import { EmulatorRegistry } from "./registry"; +import { expect } from "chai"; +import { FakeEmulator } from "./testing/fakeEmulator"; +import * as express from "express"; +import * as os from "os"; + +describe("EmulatorRegistry", () => { + afterEach(async () => { + await EmulatorRegistry.stopAll(); + }); + + it("should not report any running emulators when empty", () => { + for (const name of ALL_EMULATORS) { + expect(EmulatorRegistry.isRunning(name)).to.be.false; + } + + expect(EmulatorRegistry.listRunning()).to.be.empty; + }); + + it("should correctly return information about a running emulator", async () => { + const name = Emulators.FUNCTIONS; + const emu = await FakeEmulator.create(name); + + expect(EmulatorRegistry.isRunning(name)).to.be.false; + + await EmulatorRegistry.start(emu); + + expect(EmulatorRegistry.isRunning(name)).to.be.true; + expect(EmulatorRegistry.listRunning()).to.eql([name]); + expect(EmulatorRegistry.get(name)).to.eql(emu); + expect(EmulatorRegistry.getInfo(name)!.port).to.eql(emu.getInfo().port); + }); + + it("once stopped, an emulator is no longer running", async () => { + const name = Emulators.FUNCTIONS; + const emu = await FakeEmulator.create(name); + + expect(EmulatorRegistry.isRunning(name)).to.be.false; + await EmulatorRegistry.start(emu); + expect(EmulatorRegistry.isRunning(name)).to.be.true; + await EmulatorRegistry.stop(name); + expect(EmulatorRegistry.isRunning(name)).to.be.false; + }); + + describe("#url", () => { + // Only run IPv4 / IPv6 tests if supported respectively. + let ipv4Supported = false; + let ipv6Supported = false; + before(() => { + for (const ifaces of Object.values(os.networkInterfaces())) { + if (!ifaces) { + continue; + } + for (const iface of ifaces) { + switch (iface.family) { + case "IPv4": + ipv4Supported = true; + break; + case "IPv6": + ipv6Supported = true; + break; + } + } + } + }); + + const name = Emulators.FUNCTIONS; + afterEach(() => { + return EmulatorRegistry.stopAll(); + }); + + it("should craft URL from host and port in registry", async () => { + const emu = await FakeEmulator.create(name); + await EmulatorRegistry.start(emu); + + expect(EmulatorRegistry.url(name).host).to.eql(`${emu.getInfo().host}:${emu.getInfo().port}`); + }); + + it("should quote IPv6 addresses", async function (this) { + if (!ipv6Supported) { + return this.skip(); + } + const emu = await FakeEmulator.create(name, "::1"); + await EmulatorRegistry.start(emu); + + expect(EmulatorRegistry.url(name).host).to.eql(`[::1]:${emu.getInfo().port}`); + }); + + it("should use 127.0.0.1 instead of 0.0.0.0", async function (this) { + if (!ipv4Supported) { + return this.skip(); + } + + const emu = await FakeEmulator.create(name, "0.0.0.0"); + await EmulatorRegistry.start(emu); + + expect(EmulatorRegistry.url(name).host).to.eql(`127.0.0.1:${emu.getInfo().port}`); + }); + + it("should use ::1 instead of ::", async function (this) { + if (!ipv6Supported) { + return this.skip(); + } + + const emu = await FakeEmulator.create(name, "::"); + await EmulatorRegistry.start(emu); + + expect(EmulatorRegistry.url(name).host).to.eql(`[::1]:${emu.getInfo().port}`); + }); + + it("should use protocol from request if available", async () => { + const emu = await FakeEmulator.create(name); + await EmulatorRegistry.start(emu); + + const req = { protocol: "https", headers: {} } as express.Request; + expect(EmulatorRegistry.url(name, req).protocol).to.eql(`https:`); + expect(EmulatorRegistry.url(name, req).host).to.eql( + `${emu.getInfo().host}:${emu.getInfo().port}`, + ); + }); + + it("should use host from request if available", async () => { + const emu = await FakeEmulator.create(name); + await EmulatorRegistry.start(emu); + + const hostFromHeader = "mydomain.example.test:9999"; + const req = { + protocol: "http", + headers: { host: hostFromHeader }, + } as express.Request; + expect(EmulatorRegistry.url(name, req).host).to.eql(hostFromHeader); + }); + }); +}); diff --git a/src/emulator/registry.ts b/src/emulator/registry.ts index 7c9ac400678..3ee7281d9f0 100644 --- a/src/emulator/registry.ts +++ b/src/emulator/registry.ts @@ -1,9 +1,19 @@ -import { ALL_EMULATORS, EmulatorInstance, Emulators, EmulatorInfo } from "./types"; +import { + ALL_EMULATORS, + EmulatorInstance, + Emulators, + EmulatorInfo, + DownloadableEmulatorDetails, + DownloadableEmulators, +} from "./types"; import { FirebaseError } from "../error"; import * as portUtils from "./portUtils"; import { Constants } from "./constants"; import { EmulatorLogger } from "./emulatorLogger"; - +import * as express from "express"; +import { connectableHostname } from "../utils"; +import { Client, ClientOptions } from "../apiv2"; +import { get as getDownloadableEmulatorDetails } from "./downloadableEmulators"; /** * Static registry for running emulators to discover each other. * @@ -22,24 +32,34 @@ export class EmulatorRegistry { // Start the emulator and wait for it to grab its assigned port. await instance.start(); - - const info = instance.getInfo(); - await portUtils.waitForPortClosed(info.port, info.host); + // No need to wait for the Extensions emulator to close its port, since it runs on the Functions emulator. + if (instance.getName() !== Emulators.EXTENSIONS) { + const info = instance.getInfo(); + await portUtils.waitForPortUsed(info.port, connectableHostname(info.host), info.timeout); + } } static async stop(name: Emulators): Promise { EmulatorLogger.forEmulator(name).logLabeled( "BULLET", name, - `Stopping ${Constants.description(name)}` + `Stopping ${Constants.description(name)}`, ); const instance = this.get(name); if (!instance) { return; } - await instance.stop(); - this.clear(instance.getName()); + try { + await instance.stop(); + this.clear(instance.getName()); + } catch (e: any) { + EmulatorLogger.forEmulator(name).logLabeled( + "WARN", + name, + `Error stopping ${Constants.description(name)}`, + ); + } } static async stopAll(): Promise { @@ -48,9 +68,13 @@ export class EmulatorRegistry { // once shutdown starts ui: 0, + // The Extensions emulator runs on the same process as the Functions emulator + // so this is a no-op. We put this before functions for future proofing, since + // the Extensions emulator depends on the Functions emulator. + extensions: 1, // Functions is next since it has side effects and // dependencies across all the others - functions: 1, + functions: 1.1, // Hosting is next because it can trigger functions. hosting: 2, @@ -62,6 +86,8 @@ export class EmulatorRegistry { pubsub: 3.2, auth: 3.3, storage: 3.5, + eventarc: 3.6, + dataconnect: 3.7, // Hub shuts down once almost everything else is done hub: 4, @@ -75,19 +101,15 @@ export class EmulatorRegistry { }); for (const name of emulatorsToStop) { - try { - await this.stop(name); - } catch (e) { - EmulatorLogger.forEmulator(name).logLabeled( - "WARN", - name, - `Error stopping ${Constants.description(name)}` - ); - } + await this.stop(name); } } static isRunning(emulator: Emulators): boolean { + if (emulator === Emulators.EXTENSIONS) { + // Check if the functions emulator is also running - if not, the Extensions emulator won't work. + return this.INSTANCES.get(emulator) !== undefined && this.isRunning(Emulators.FUNCTIONS); + } const instance = this.INSTANCES.get(emulator); return instance !== undefined; } @@ -106,33 +128,74 @@ export class EmulatorRegistry { return this.INSTANCES.get(emulator); } + /** + * Get information about an emulator. Use `url` instead for creating URLs. + */ static getInfo(emulator: Emulators): EmulatorInfo | undefined { - const instance = this.INSTANCES.get(emulator); - if (!instance) { + const info = EmulatorRegistry.get(emulator)?.getInfo(); + if (!info) { return undefined; } + return { + ...info, + host: connectableHostname(info.host), + }; + } - return instance.getInfo(); + static getDetails(emulator: DownloadableEmulators): DownloadableEmulatorDetails { + return getDownloadableEmulatorDetails(emulator); } - static getInfoHostString(info: EmulatorInfo): string { - const { host, port } = info; + /** + * Return a URL object with the emulator protocol, host, and port populated. + * + * Need to make an API request? Use `.client` instead. + * + * @param emulator for retrieving host and port from the registry + * @param req if provided, will prefer reflecting back protocol+host+port from + * the express request (if header available) instead of registry + * @return a WHATWG URL object with .host set to the emulator host + port + */ + static url(emulator: Emulators, req?: express.Request): URL { + // WHATWG URL API has no way to create from parts, so let's use a minimal + // working URL to start. (Let's avoid legacy Node.js `url.format`.) + const url = new URL("http://unknown/"); + + if (req) { + url.protocol = req.protocol; + // Try the Host request header, since it contains hostname + port already + // and has been proved to work (since we've got the client request). + const host = req.headers.host; + if (host) { + url.host = host; + return url; + } + } - // Quote IPv6 addresses - if (host.includes(":")) { - return `[${host}]:${port}`; + // Fall back to the host and port from registry. This provides a reasonable + // value in most cases but may not work if the client needs to connect via + // another host, e.g. in Dockers or behind reverse proxies. + const info = EmulatorRegistry.getInfo(emulator); + if (info) { + if (info.host.includes(":")) { + url.hostname = `[${info.host}]`; // IPv6 addresses need to be quoted. + } else { + url.hostname = info.host; + } + url.port = info.port.toString(); } else { - return `${host}:${port}`; + throw new Error(`Cannot determine host and port of ${emulator}`); } - } - static getPort(emulator: Emulators): number | undefined { - const instance = this.INSTANCES.get(emulator); - if (!instance) { - return undefined; - } + return url; + } - return instance.getInfo().port; + static client(emulator: Emulators, options: Omit = {}): Client { + return new Client({ + urlPrefix: EmulatorRegistry.url(emulator).toString(), + auth: false, + ...options, + }); } private static INSTANCES: Map = new Map(); diff --git a/src/emulator/shared/request.ts b/src/emulator/shared/request.ts new file mode 100644 index 00000000000..75a905dc148 --- /dev/null +++ b/src/emulator/shared/request.ts @@ -0,0 +1,18 @@ +import { Request } from "express"; + +/** Returns the body of a {@link Request} as a {@link Buffer}. */ +export async function reqBodyToBuffer(req: Request): Promise { + if (req.body instanceof Buffer) { + return Buffer.from(req.body); + } + const bufs: Buffer[] = []; + req.on("data", (data) => { + bufs.push(data); + }); + await new Promise((resolve) => { + req.on("end", () => { + resolve(); + }); + }); + return Buffer.concat(bufs); +} diff --git a/src/emulator/storage/README.md b/src/emulator/storage/README.md new file mode 100644 index 00000000000..63a57b057e5 --- /dev/null +++ b/src/emulator/storage/README.md @@ -0,0 +1,52 @@ +# Firebase Storage emulator + +The Firebase Storage Emulator can be used to help test and develop your Firebase project. + +To get started with the Firebase Storage emulator or see what it can be used for, +check out the [documentation](https://firebase.google.com/docs/emulator-suite/connect_storage). + +## Testing + +The Firebase Storage Emulator has a full suite of unit and integration tests. + +To run integration tests run the following command: + +```base +npm run test:storage-emulator-integration +``` + +To run unit tests run the following command: + +```base +npm run mocha src/emulator/storage +``` + +## Developing locally + +#### Link your local repository to your environment + +After cloning the project, use `npm link` to globally link your local +repository: + +```bash +git clone git@github.com:firebase/firebase-tools.git +cd firebase-tools +npm install # must be run the first time you clone +npm link # installs dependencies, runs a build, links it into the environment +``` + +This link makes the `firebase` command execute against the code in your local +repository, rather than your globally installed version of `firebase-tools`. +This is great for manual testing. + +Alternatively adding `"firebase-tools": "file:./YOUR_PATH_HERE/firebase-tools"` +into another repo's package.json dependencies will also execute code against the local repository. + +#### Unlink your local repository + +To un-link `firebase-tools` from your local repository, you can do any of the +following: + +- run `npm uninstall -g firebase-tools` +- run `npm unlink` in your local repository +- re-install `firebase-tools` globally using `npm i -g firebase-tools` diff --git a/src/emulator/storage/apis/firebase.ts b/src/emulator/storage/apis/firebase.ts index d1c1f9034b0..710d7a9df07 100644 --- a/src/emulator/storage/apis/firebase.ts +++ b/src/emulator/storage/apis/firebase.ts @@ -1,52 +1,21 @@ import { EmulatorLogger } from "../../emulatorLogger"; import { Emulators } from "../../types"; -import { gunzipSync } from "zlib"; -import { OutgoingFirebaseMetadata, RulesResourceMetadata, StoredFileMetadata } from "../metadata"; -import * as mime from "mime"; +import * as uuid from "uuid"; +import { IncomingMetadata, OutgoingFirebaseMetadata, StoredFileMetadata } from "../metadata"; import { Request, Response, Router } from "express"; import { StorageEmulator } from "../index"; +import { sendFileBytes } from "./shared"; import { EmulatorRegistry } from "../../registry"; -import { StorageRulesetInstance } from "../rules/runtime"; -import { RulesetOperationMethod } from "../rules/types"; - -async function isPermitted(opts: { - ruleset?: StorageRulesetInstance; - file: { - before?: RulesResourceMetadata; - after?: RulesResourceMetadata; - }; - path: string; - method: RulesetOperationMethod; - authorization?: string; -}): Promise { - if (!opts.ruleset) { - EmulatorLogger.forEmulator(Emulators.STORAGE).log( - "WARN", - `Can not process SDK request with no loaded ruleset` - ); - return false; - } - - // Skip auth for UI - if (["Bearer owner", "Firebase owner"].includes(opts.authorization || "")) { - return true; - } - - const { permitted, issues } = await opts.ruleset.verify({ - method: opts.method, - path: opts.path, - file: opts.file, - token: opts.authorization ? opts.authorization.split(" ")[1] : undefined, - }); - - if (issues.exist()) { - issues.all.forEach((warningOrError) => { - EmulatorLogger.forEmulator(Emulators.STORAGE).log("WARN", warningOrError); - }); - } - - return !!permitted; -} +import { parseObjectUploadMultipartRequest } from "../multipart"; +import { NotFoundError, ForbiddenError } from "../errors"; +import { + NotCancellableError, + Upload, + UploadNotActiveError, + UploadPreviouslyFinalizedError, +} from "../upload"; +import { reqBodyToBuffer } from "../../shared/request"; +import { ListObjectsResponse } from "../files"; /** * @param emulator @@ -54,11 +23,11 @@ async function isPermitted(opts: { export function createFirebaseEndpoints(emulator: StorageEmulator): Router { // eslint-disable-next-line new-cap const firebaseStorageAPI = Router(); - const { storageLayer } = emulator; + const { storageLayer, uploadService } = emulator; if (process.env.STORAGE_EMULATOR_DEBUG) { firebaseStorageAPI.use((req, res, next) => { - console.log("--------------INCOMING REQUEST--------------"); + console.log("--------------INCOMING FIREBASE REQUEST--------------"); console.log(`${req.method.toUpperCase()} ${req.path}`); console.log("-- query:"); console.log(JSON.stringify(req.query, undefined, 2)); @@ -102,11 +71,14 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { }); } - firebaseStorageAPI.use((req, res, next) => { - if (!emulator.rules) { + // Automatically create a bucket for any route which uses a bucket + firebaseStorageAPI.use(/.*\/b\/(.+?)\/.*/, (req, res, next) => { + const bucketId = req.params[0]; + storageLayer.createBucket(bucketId); + if (!emulator.rulesManager.getRuleset(bucketId)) { EmulatorLogger.forEmulator(Emulators.STORAGE).log( "WARN", - "Permission denied because no Storage ruleset is currently loaded, check your rules for syntax errors." + "Permission denied because no Storage ruleset is currently loaded, check your rules for syntax errors.", ); return res.status(403).json({ error: { @@ -115,509 +87,420 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { }, }); } - - next(); - }); - - // Automatically create a bucket for any route which uses a bucket - firebaseStorageAPI.use(/.*\/b\/(.+?)\/.*/, (req, res, next) => { - storageLayer.createBucket(req.params[0]); next(); }); firebaseStorageAPI.get("/b/:bucketId/o/:objectId", async (req, res) => { - const decodedObjectId = decodeURIComponent(req.params.objectId); - const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/"); - const md = storageLayer.getMetadata(req.params.bucketId, decodedObjectId); - - const rulesFiles: { - before?: RulesResourceMetadata; - } = {}; - - if (md) { - rulesFiles.before = md.asRulesResource(); - } - - // Query values are used for GETs from Web SDKs - const isPermittedViaHeader = await isPermitted({ - ruleset: emulator.rules, - method: RulesetOperationMethod.GET, - path: operationPath, - file: rulesFiles, - authorization: req.header("authorization"), - }); - - // Token headers are used for GETs from Mobile SDKs - const isPermittedViaToken = - req.query.token && md && md.downloadTokens.includes(req.query.token.toString()); - - const isRequestPermitted: boolean = isPermittedViaHeader || !!isPermittedViaToken; - - if (!isRequestPermitted) { - res.sendStatus(403); - return; - } - - if (!md) { - res.sendStatus(404); - return; - } - - let isGZipped = false; - if (md.contentEncoding == "gzip") { - isGZipped = true; - } - - if (req.query.alt == "media") { - let data = storageLayer.getBytes(req.params.bucketId, req.params.objectId); - if (!data) { - res.sendStatus(404); - return; - } - - if (isGZipped) { - data = gunzipSync(data); - } - - res.setHeader("Accept-Ranges", "bytes"); - res.setHeader("Content-Type", md.contentType); - setObjectHeaders(res, md, { "Content-Encoding": isGZipped ? "identity" : undefined }); - - const byteRange = [...(req.header("range") || "").split("bytes="), "", ""]; - - const [rangeStart, rangeEnd] = byteRange[1].split("-"); - - if (rangeStart) { - const range = { - start: parseInt(rangeStart), - end: rangeEnd ? parseInt(rangeEnd) : data.byteLength, - }; - res.setHeader("Content-Range", `bytes ${range.start}-${range.end - 1}/${data.byteLength}`); - res.status(206).end(data.slice(range.start, range.end)); - } else { - res.end(data); + let metadata: StoredFileMetadata; + let data: Buffer; + try { + // Both object data and metadata get can use the same handler since they share auth logic. + ({ metadata, data } = await storageLayer.getObject({ + bucketId: req.params.bucketId, + decodedObjectId: decodeURIComponent(req.params.objectId), + authorization: req.header("authorization"), + downloadToken: req.query.token?.toString(), + })); + } catch (err) { + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } else if (err instanceof ForbiddenError) { + return res.status(403).json({ + error: { + code: 403, + message: `Permission denied. No READ permission.`, + }, + }); } - - return; + throw err; } - if (!md.downloadTokens.length) { - md.addDownloadToken(); + if (metadata.downloadTokens.length === 0) { + metadata.addDownloadToken(/* shouldTrigger = */ true); } - res.json(new OutgoingFirebaseMetadata(md)); - }); - - const handleMetadataUpdate = async (req: Request, res: Response) => { - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if (!md) { - res.sendStatus(404); - return; - } - - const decodedObjectId = decodeURIComponent(req.params.objectId); - const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/"); - - if ( - !(await isPermitted({ - ruleset: emulator.rules, - method: RulesetOperationMethod.UPDATE, - path: operationPath, - authorization: req.header("authorization"), - file: { - before: md.asRulesResource(), - after: md.asRulesResource(req.body), // TODO - }, - })) - ) { - return res.status(403).json({ - error: { - code: 403, - message: `Permission denied. No WRITE permission.`, - }, - }); + // Object data request + if (req.query.alt === "media") { + return sendFileBytes(metadata, data, req, res); } - md.update(req.body); - - setObjectHeaders(res, md); - const outgoingMetadata = new OutgoingFirebaseMetadata(md); - res.json(outgoingMetadata); - return; - }; + // Object metadata request + return res.json(new OutgoingFirebaseMetadata(metadata)); + }); // list object handler firebaseStorageAPI.get("/b/:bucketId/o", async (req, res) => { - let maxRes = undefined; - if (req.query.maxResults) { - maxRes = +req.query.maxResults.toString(); + const maxResults = req.query.maxResults?.toString(); + let listResponse: ListObjectsResponse; + // The prefix query param must be empty or end in a "/" + let prefix = ""; + if (req.query.prefix) { + prefix = req.query.prefix.toString(); + if (prefix.charAt(prefix.length - 1) !== "/") { + return res.status(400).json({ + error: { + code: 400, + message: + "The prefix parameter is required to be empty or ends with a single / character.", + }, + }); + } } - const delimiter = req.query.delimiter ? req.query.delimiter.toString() : "/"; - const pageToken = req.query.pageToken ? req.query.pageToken.toString() : undefined; - const prefix = req.query.prefix ? req.query.prefix.toString() : ""; - - const operationPath = ["b", req.params.bucketId, "o", prefix].join("/"); - - if ( - !(await isPermitted({ - ruleset: emulator.rules, - method: RulesetOperationMethod.LIST, - path: operationPath, - file: {}, + try { + listResponse = await storageLayer.listObjects({ + bucketId: req.params.bucketId, + prefix: prefix, + delimiter: req.query.delimiter ? req.query.delimiter.toString() : "", + pageToken: req.query.pageToken?.toString(), + maxResults: maxResults ? +maxResults : undefined, authorization: req.header("authorization"), - })) - ) { - return res.status(403).json({ - error: { - code: 403, - message: `Permission denied. No LIST permission.`, - }, }); - } - - res.json( - storageLayer.listItemsAndPrefixes(req.params.bucketId, prefix, delimiter, pageToken, maxRes) - ); - }); - - const handleUpload = async (req: Request, res: Response) => { - if (req.query.create_token || req.query.delete_token) { - const decodedObjectId = decodeURIComponent(req.params.objectId); - const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/"); - - const mdBefore = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if ( - !(await isPermitted({ - ruleset: emulator.rules, - method: RulesetOperationMethod.UPDATE, - path: operationPath, - authorization: req.header("authorization"), - file: { - before: mdBefore?.asRulesResource(), - // TODO: before and after w/ metadata change - }, - })) - ) { + } catch (err) { + if (err instanceof ForbiddenError) { return res.status(403).json({ error: { code: 403, - message: `Permission denied. No WRITE permission.`, + message: `Permission denied. No LIST permission.`, }, }); } + throw err; + } + return res.status(200).json({ + nextPageToken: listResponse.nextPageToken, + prefixes: (listResponse.prefixes ?? []).filter(isValidPrefix), + items: (listResponse.items ?? []) + .filter((item) => isValidNonEncodedPathString(item.name)) + .map((item) => { + return { name: item.name, bucket: item.bucket }; + }), + }); + }); - if (!mdBefore) { - return res.status(404).json({ - error: { - code: 404, - message: `Request object can not be found`, - }, - }); + const handleUpload = async (req: Request, res: Response) => { + const bucketId = req.params.bucketId; + const objectId: string | null = req.params.objectId + ? decodeURIComponent(req.params.objectId) + : req.query.name?.toString() || null; + const uploadType = req.header("x-goog-upload-protocol")?.toString(); + + async function finalizeOneShotUpload(upload: Upload) { + // Set default download token if it isn't available. + if (!upload.metadata?.metadata?.firebaseStorageDownloadTokens) { + const customMetadata = { + ...(upload.metadata?.metadata || {}), + firebaseStorageDownloadTokens: uuid.v4(), + }; + upload.metadata = { ...(upload.metadata || {}), metadata: customMetadata }; } - - const createTokenParam = req.query["create_token"]; - const deleteTokenParam = req.query["delete_token"]; - let md: StoredFileMetadata | undefined; - - if (createTokenParam) { - if (createTokenParam != "true") { - res.sendStatus(400); - return; + let metadata: StoredFileMetadata; + try { + metadata = await storageLayer.uploadObject(upload); + } catch (err) { + if (err instanceof ForbiddenError) { + res.header("x-goog-upload-status", "final"); + uploadService.setResponseCode(upload.id, 403); + return res.status(403).json({ + error: { + code: 403, + message: "Permission denied. No WRITE permission.", + }, + }); } - md = storageLayer.addDownloadToken(req.params.bucketId, req.params.objectId); - } else if (deleteTokenParam) { - md = storageLayer.deleteDownloadToken( - req.params.bucketId, - req.params.objectId, - deleteTokenParam.toString() - ); + throw err; } - - if (!md) { - res.sendStatus(404); - return; + if (!metadata.contentDisposition) { + metadata.contentDisposition = "inline"; } - - setObjectHeaders(res, md); - return res.json(new OutgoingFirebaseMetadata(md)); - } - - if (!req.query.name) { - res.sendStatus(400); - return; + return res.status(200).json(new OutgoingFirebaseMetadata(metadata)); } - const name = req.query.name.toString(); - const uploadType = req.header("x-goog-upload-protocol"); - - if (uploadType == "multipart") { - const contentType = req.header("content-type"); - if (!contentType || !contentType.startsWith("multipart/related")) { - res.sendStatus(400); - return; - } - - const boundary = `--${contentType.split("boundary=")[1]}`; - const bodyString = req.body.toString(); - const bodyStringParts = bodyString.split(boundary).filter((v: string) => v); - - const metadataString = bodyStringParts[0].split("\r\n")[3]; - const blobParts = bodyStringParts[1].split("\r\n"); - const blobContentTypeString = blobParts[1]; - if (!blobContentTypeString || !blobContentTypeString.startsWith("Content-Type: ")) { - res.sendStatus(400); - return; - } - const blobContentType = blobContentTypeString.slice("Content-Type: ".length); - const bodyBuffer = req.body as Buffer; - - const metadataSegment = `${boundary}${bodyString.split(boundary)[1]}`; - const dataSegment = `${boundary}${bodyString.split(boundary).slice(2)[0]}`; - const dataSegmentHeader = (dataSegment.match(/.+Content-Type:.+?\r\n\r\n/s) || [])[0]; - - if (!dataSegmentHeader) { - res.sendStatus(400); - return; - } - - const bufferOffset = metadataSegment.length + dataSegmentHeader.length; - - const blobBytes = Buffer.from(bodyBuffer.slice(bufferOffset, -`\r\n${boundary}--`.length)); - const md = storageLayer.oneShotUpload( - req.params.bucketId, - name, - blobContentType, - JSON.parse(metadataString), - Buffer.from(blobBytes) - ); - - if (!md) { - res.sendStatus(400); - return; - } - - const operationPath = ["b", req.params.bucketId, "o", name].join("/"); - - if ( - !(await isPermitted({ - ruleset: emulator.rules, - // TODO: This will be either create or update - method: RulesetOperationMethod.CREATE, - path: operationPath, - authorization: req.header("authorization"), - file: { - after: md?.asRulesResource(), - }, - })) - ) { - storageLayer.deleteFile(md?.bucket, md?.name); - return res.status(403).json({ - error: { - code: 403, - message: `Permission denied. No WRITE permission.`, - }, - }); - } - - if (md.downloadTokens.length == 0) { - md.addDownloadToken(); - } - - res.json(new OutgoingFirebaseMetadata(md)); - return; - } else { - const operationPath = ["b", req.params.bucketId, "o", name].join("/"); + // Resumable upload + // sdk can set uploadType or just set upload command to indicate resumable upload + if (uploadType === "resumable" || req.header("x-goog-upload-command")) { const uploadCommand = req.header("x-goog-upload-command"); if (!uploadCommand) { res.sendStatus(400); return; } - if (uploadCommand == "start") { - let objectContentType = - req.header("x-goog-upload-header-content-type") || - req.header("x-goog-upload-content-type"); - if (!objectContentType) { - const mimeTypeFromName = mime.getType(name); - if (!mimeTypeFromName) { - objectContentType = "application/octet-stream"; - } else { - objectContentType = mimeTypeFromName; - } + if (uploadCommand === "start") { + if (!objectId) { + res.sendStatus(400); + return; } - - const upload = storageLayer.startUpload( - req.params.bucketId, - name, - objectContentType, - req.body - ); - - storageLayer.uploadBytes(upload.uploadId, Buffer.alloc(0)); - - const emulatorInfo = EmulatorRegistry.getInfo(Emulators.STORAGE); + const upload = uploadService.startResumableUpload({ + bucketId, + objectId, + metadata: req.body, + // Store auth header for use in the finalize request + authorization: req.header("authorization"), + }); res.header("x-goog-upload-chunk-granularity", "10000"); res.header("x-goog-upload-control-url", ""); res.header("x-goog-upload-status", "active"); - res.header( - "x-goog-upload-url", - `http://${req.hostname}:${emulatorInfo?.port}/v0/b/${req.params.bucketId}/o?name=${req.query.name}&upload_id=${upload.uploadId}&upload_protocol=resumable` - ); - res.header("x-gupload-uploadid", upload.uploadId); - - res.status(200).send(); - return; + res.header("x-gupload-uploadid", upload.id); + + const uploadUrl = EmulatorRegistry.url(Emulators.STORAGE, req); + uploadUrl.pathname = `/v0/b/${bucketId}/o`; + uploadUrl.searchParams.set("name", objectId); + uploadUrl.searchParams.set("upload_id", upload.id); + uploadUrl.searchParams.set("upload_protocol", "resumable"); + res.header("x-goog-upload-url", uploadUrl.toString()); + return res.sendStatus(200); } if (!req.query.upload_id) { - res.sendStatus(400); - return; + return res.sendStatus(400); } const uploadId = req.query.upload_id.toString(); - if (uploadCommand == "query") { - const upload = storageLayer.queryUpload(uploadId); - if (!upload) { - res.sendStatus(400); - return; + if (uploadCommand === "query") { + let upload: Upload; + try { + upload = uploadService.getResumableUpload(uploadId); + } catch (err) { + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } + throw err; } - - res.header("X-Goog-Upload-Size-Received", upload.currentBytesUploaded.toString()); - res.sendStatus(200); - return; + res.header("X-Goog-Upload-Size-Received", upload.size.toString()); + res.header("x-goog-upload-status", upload.status); + return res.sendStatus(200); } - if (uploadCommand == "cancel") { - const upload = storageLayer.cancelUpload(uploadId); - if (!upload) { - res.sendStatus(400); - return; + if (uploadCommand === "cancel") { + try { + uploadService.cancelResumableUpload(uploadId); + } catch (err) { + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } else if (err instanceof NotCancellableError) { + return res.sendStatus(400); + } + throw err; } - res.sendStatus(200); - return; + return res.sendStatus(200); } - let upload; if (uploadCommand.includes("upload")) { - if (!(req.body instanceof Buffer)) { - const bufs: Buffer[] = []; - req.on("data", (data) => { - bufs.push(data); - }); - - await new Promise((resolve) => { - req.on("end", () => { - req.body = Buffer.concat(bufs); - resolve(); - }); - }); + let upload: Upload; + try { + upload = uploadService.continueResumableUpload(uploadId, await reqBodyToBuffer(req)); + } catch (err) { + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } else if (err instanceof UploadNotActiveError) { + return res.sendStatus(400); + } + throw err; } + if (!uploadCommand.includes("finalize")) { + res.header("x-goog-upload-status", "active"); + res.header("x-gupload-uploadid", upload.id); + return res.sendStatus(200); + } + // Intentional fall through to handle "upload, finalize" case. + } - upload = storageLayer.uploadBytes(uploadId, req.body); - - if (!upload) { - res.sendStatus(400); - return; + if (uploadCommand.includes("finalize")) { + let upload: Upload; + try { + upload = uploadService.finalizeResumableUpload(uploadId); + } catch (err) { + if (err instanceof NotFoundError) { + uploadService.setResponseCode(uploadId, 404); + return res.sendStatus(404); + } else if (err instanceof UploadNotActiveError) { + uploadService.setResponseCode(uploadId, 400); + return res.sendStatus(400); + } else if (err instanceof UploadPreviouslyFinalizedError) { + res.header("x-goog-upload-status", "final"); + return res.sendStatus(uploadService.getPreviousResponseCode(uploadId)); + } + throw err; } + res.header("x-goog-upload-status", "final"); + return await finalizeOneShotUpload(upload); + } + } - res.header("x-goog-upload-status", "active"); - res.header("x-gupload-uploadid", upload.uploadId); + if (!objectId) { + res.sendStatus(400); + return; + } + + // Multipart upload + if (uploadType === "multipart") { + const contentTypeHeader = req.header("content-type"); + if (!contentTypeHeader) { + return res.sendStatus(400); } - if (uploadCommand.includes("finalize")) { - const finalizedUpload = storageLayer.finalizeUpload(uploadId); - if (!finalizedUpload) { - res.sendStatus(400); - return; + let metadataRaw: string; + let dataRaw: Buffer; + try { + ({ metadataRaw, dataRaw } = parseObjectUploadMultipartRequest( + contentTypeHeader!, + await reqBodyToBuffer(req), + )); + } catch (err) { + if (err instanceof Error) { + // Matches server error text formatting. + return res.status(400).send(err.message); } - upload = finalizedUpload.upload; + throw err; + } + const upload = uploadService.multipartUpload({ + bucketId, + objectId, + metadata: JSON.parse(metadataRaw), + dataRaw: dataRaw, + authorization: req.header("authorization"), + }); + return await finalizeOneShotUpload(upload); + } - res.header("x-goog-upload-status", "final"); + // Default to media (data-only) upload protocol. + const upload = uploadService.mediaUpload({ + bucketId: req.params.bucketId, + objectId: objectId, + dataRaw: await reqBodyToBuffer(req), + authorization: req.header("authorization"), + }); + return await finalizeOneShotUpload(upload); + }; - // For resumable uploads, we check auth on finalization in case of byte-dependant rules - if ( - !(await isPermitted({ - ruleset: emulator.rules, - // TODO This will be either create or update - method: RulesetOperationMethod.CREATE, - path: operationPath, - authorization: req.header("authorization"), - file: { - after: storageLayer.getMetadata(req.params.bucketId, name)?.asRulesResource(), + const handleTokenRequest = (req: Request, res: Response) => { + if (!req.query.create_token && !req.query.delete_token) { + return res.sendStatus(400); + } + const bucketId = req.params.bucketId; + const decodedObjectId = decodeURIComponent(req.params.objectId); + const authorization = req.header("authorization"); + let metadata: StoredFileMetadata; + if (req.query.create_token) { + if (req.query.create_token !== "true") { + return res.sendStatus(400); + } + try { + metadata = storageLayer.createDownloadToken({ + bucketId, + decodedObjectId, + authorization, + }); + } catch (err) { + if (err instanceof ForbiddenError) { + return res.status(403).json({ + error: { + code: 403, + message: `Missing admin credentials.`, }, - })) - ) { - storageLayer.deleteFile(upload.bucketId, name); + }); + } + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } + throw err; + } + } else { + // delete download token + try { + metadata = storageLayer.deleteDownloadToken({ + bucketId, + decodedObjectId, + token: req.query["delete_token"]?.toString() ?? "", + authorization, + }); + } catch (err) { + if (err instanceof ForbiddenError) { return res.status(403).json({ error: { code: 403, - message: `Permission denied. No WRITE permission.`, + message: `Missing admin credentials.`, }, }); } - - const md = finalizedUpload.file.metadata; - if (md.downloadTokens.length == 0) { - md.addDownloadToken(); + if (err instanceof NotFoundError) { + return res.sendStatus(404); } + throw err; + } + } + setObjectHeaders(res, metadata); + return res.json(new OutgoingFirebaseMetadata(metadata)); + }; - res.json(new OutgoingFirebaseMetadata(finalizedUpload.file.metadata)); - } else if (!upload) { - res.sendStatus(400); - return; - } else { - res.sendStatus(200); + const handleObjectPostRequest = async (req: Request, res: Response) => { + if (req.query.create_token || req.query.delete_token) { + return handleTokenRequest(req, res); + } + return handleUpload(req, res); + }; + + const handleMetadataUpdate = async (req: Request, res: Response) => { + let metadata: StoredFileMetadata; + try { + metadata = await storageLayer.updateObjectMetadata({ + bucketId: req.params.bucketId, + decodedObjectId: decodeURIComponent(req.params.objectId), + metadata: req.body as IncomingMetadata, + authorization: req.header("authorization"), + }); + } catch (err) { + if (err instanceof ForbiddenError) { + return res.status(403).json({ + error: { + code: 403, + message: `Permission denied. No WRITE permission.`, + }, + }); } + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } + throw err; } + setObjectHeaders(res, metadata); + return res.json(new OutgoingFirebaseMetadata(metadata)); }; - // update metata handler firebaseStorageAPI.patch("/b/:bucketId/o/:objectId", handleMetadataUpdate); firebaseStorageAPI.put("/b/:bucketId/o/:objectId?", async (req, res) => { switch (req.header("x-http-method-override")?.toLowerCase()) { case "patch": return handleMetadataUpdate(req, res); default: - return handleUpload(req, res); + return handleObjectPostRequest(req, res); } }); - firebaseStorageAPI.post("/b/:bucketId/o/:objectId?", handleUpload); - firebaseStorageAPI.delete("/b/:bucketId/o/:objectId", async (req, res) => { - const decodedObjectId = decodeURIComponent(req.params.objectId); - const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/"); + firebaseStorageAPI.post("/b/:bucketId/o/:objectId?", handleObjectPostRequest); - if ( - !(await isPermitted({ - ruleset: emulator.rules, - method: RulesetOperationMethod.DELETE, - path: operationPath, + firebaseStorageAPI.delete("/b/:bucketId/o/:objectId", async (req, res) => { + try { + await storageLayer.deleteObject({ + bucketId: req.params.bucketId, + decodedObjectId: decodeURIComponent(req.params.objectId), authorization: req.header("authorization"), - file: { - // TODO load before metadata - }, - })) - ) { - return res.status(403).json({ - error: { - code: 403, - message: `Permission denied. No WRITE permission.`, - }, }); + } catch (err) { + if (err instanceof ForbiddenError) { + return res.status(403).json({ + error: { + code: 403, + message: `Permission denied. No WRITE permission.`, + }, + }); + } + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } + throw err; } - - const md = storageLayer.getMetadata(req.params.bucketId, decodedObjectId); - - if (!md) { - res.sendStatus(404); - return; - } - - storageLayer.deleteFile(req.params.bucketId, req.params.objectId); - res.sendStatus(200); + res.sendStatus(204); }); firebaseStorageAPI.get("/", (req, res) => { @@ -627,26 +510,42 @@ export function createFirebaseEndpoints(emulator: StorageEmulator): Router { return firebaseStorageAPI; } -function setObjectHeaders( - res: Response, - metadata: StoredFileMetadata, - headerOverride: { - "Content-Encoding": string | undefined; - } = { "Content-Encoding": undefined } -): void { - res.setHeader("Content-Disposition", metadata.contentDisposition); - - if (headerOverride["Content-Encoding"]) { - res.setHeader("Content-Encoding", headerOverride["Content-Encoding"]); - } else { +function setObjectHeaders(res: Response, metadata: StoredFileMetadata): void { + if (metadata.contentDisposition) { + res.setHeader("Content-Disposition", metadata.contentDisposition); + } + if (metadata.contentEncoding) { res.setHeader("Content-Encoding", metadata.contentEncoding); } - if (metadata.cacheControl) { res.setHeader("Cache-Control", metadata.cacheControl); } - if (metadata.contentLanguage) { res.setHeader("Content-Language", metadata.contentLanguage); } } + +function isValidPrefix(prefix: string): boolean { + // See go/firebase-storage-backend-valid-path + return isValidNonEncodedPathString(removeAtMostOneTrailingSlash(prefix)); +} + +function isValidNonEncodedPathString(path: string): boolean { + // See go/firebase-storage-backend-valid-path + if (path.startsWith("/")) { + path = path.substring(1); + } + if (!path) { + return false; + } + for (const pathSegment of path.split("/")) { + if (!pathSegment) { + return false; + } + } + return true; +} + +function removeAtMostOneTrailingSlash(path: string): string { + return path.replace(/\/$/, ""); +} diff --git a/src/emulator/storage/apis/gcloud.ts b/src/emulator/storage/apis/gcloud.ts index 640a0cc8358..3a05037c32e 100644 --- a/src/emulator/storage/apis/gcloud.ts +++ b/src/emulator/storage/apis/gcloud.ts @@ -1,107 +1,185 @@ import { Router } from "express"; -import { gunzipSync } from "zlib"; import { Emulators } from "../../types"; import { CloudStorageObjectAccessControlMetadata, CloudStorageObjectMetadata, + IncomingMetadata, StoredFileMetadata, } from "../metadata"; +import { sendFileBytes } from "./shared"; import { EmulatorRegistry } from "../../registry"; import { StorageEmulator } from "../index"; import { EmulatorLogger } from "../../emulatorLogger"; -import { StorageLayer } from "../files"; +import { GetObjectResponse, ListObjectsResponse } from "../files"; import type { Request, Response } from "express"; +import { parseObjectUploadMultipartRequest } from "../multipart"; +import { Upload, UploadNotActiveError } from "../upload"; +import { ForbiddenError, NotFoundError } from "../errors"; +import { reqBodyToBuffer } from "../../shared/request"; +import { Query } from "express-serve-static-core"; -/** - * @param emulator - * @param storage - */ export function createCloudEndpoints(emulator: StorageEmulator): Router { // eslint-disable-next-line new-cap const gcloudStorageAPI = Router(); - const { storageLayer } = emulator; + // Use Admin StorageLayer to ensure Firebase Rules validation is skipped. + const { adminStorageLayer, uploadService } = emulator; + + // Debug statements + if (process.env.STORAGE_EMULATOR_DEBUG) { + gcloudStorageAPI.use((req, res, next) => { + console.log("--------------INCOMING GCS REQUEST--------------"); + console.log(`${req.method.toUpperCase()} ${req.path}`); + console.log("-- query:"); + console.log(JSON.stringify(req.query, undefined, 2)); + console.log("-- headers:"); + console.log(JSON.stringify(req.headers, undefined, 2)); + console.log("-- body:"); + + if (req.body instanceof Buffer) { + console.log(`Buffer of ${req.body.length}`); + } else if (req.body) { + console.log(req.body); + } else { + console.log("Empty body (could be stream)"); + } + + const resJson = res.json.bind(res); + res.json = (...args: any[]) => { + console.log("-- response:"); + args.forEach((data) => console.log(JSON.stringify(data, undefined, 2))); + + return resJson.call(res, ...args); + }; + + const resSendStatus = res.sendStatus.bind(res); + res.sendStatus = (status) => { + console.log("-- response status:"); + console.log(status); + + return resSendStatus.call(res, status); + }; + + const resStatus = res.status.bind(res); + res.status = (status) => { + console.log("-- response status:"); + console.log(status); + + return resStatus.call(res, status); + }; + + next(); + }); + } // Automatically create a bucket for any route which uses a bucket gcloudStorageAPI.use(/.*\/b\/(.+?)\/.*/, (req, res, next) => { - storageLayer.createBucket(req.params[0]); + adminStorageLayer.createBucket(req.params[0]); next(); }); - gcloudStorageAPI.get("/b", (req, res) => { + gcloudStorageAPI.get("/b", async (req, res) => { res.json({ kind: "storage#buckets", - items: storageLayer.listBuckets(), + items: await adminStorageLayer.listBuckets(), }); }); gcloudStorageAPI.get( - ["/b/:bucketId/o/:objectId", "/download/storage/v1/b/:bucketId/o/:objectId"], - (req, res) => { - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if (!md) { - res.sendStatus(404); - return; + [ + "/b/:bucketId/o/:objectId", + "/download/storage/v1/b/:bucketId/o/:objectId", + "/storage/v1/b/:bucketId/o/:objectId", + ], + async (req, res) => { + let getObjectResponse: GetObjectResponse; + try { + getObjectResponse = await adminStorageLayer.getObject({ + bucketId: req.params.bucketId, + decodedObjectId: req.params.objectId, + }); + } catch (err) { + if (err instanceof NotFoundError) { + return sendObjectNotFound(req, res); + } + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; } - if (req.query.alt == "media") { - return sendFileBytes(md, storageLayer, req, res); + if (req.query.alt === "media") { + return sendFileBytes(getObjectResponse.metadata, getObjectResponse.data, req, res); } - - const outgoingMd = new CloudStorageObjectMetadata(md); - - res.json(outgoingMd).status(200).send(); - return; - } + return res.json(new CloudStorageObjectMetadata(getObjectResponse.metadata)); + }, ); - gcloudStorageAPI.patch("/b/:bucketId/o/:objectId", (req, res) => { - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if (!md) { - res.sendStatus(404); - return; + gcloudStorageAPI.patch("/b/:bucketId/o/:objectId", async (req, res) => { + let updatedMetadata: StoredFileMetadata; + try { + updatedMetadata = await adminStorageLayer.updateObjectMetadata({ + bucketId: req.params.bucketId, + decodedObjectId: req.params.objectId, + metadata: req.body as IncomingMetadata, + }); + } catch (err) { + if (err instanceof NotFoundError) { + return sendObjectNotFound(req, res); + } + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; } - - md.update(req.body); - - const outgoingMetadata = new CloudStorageObjectMetadata(md); - res.json(outgoingMetadata).status(200).send(); - return; + return res.json(new CloudStorageObjectMetadata(updatedMetadata)); }); - gcloudStorageAPI.get("/b/:bucketId/o", (req, res) => { + gcloudStorageAPI.get(["/b/:bucketId/o", "/storage/v1/b/:bucketId/o"], async (req, res) => { + let listResponse: ListObjectsResponse; // TODO validate that all query params are single strings and are not repeated. - let maxRes = undefined; - if (req.query.maxResults) { - maxRes = +req.query.maxResults.toString(); + try { + listResponse = await adminStorageLayer.listObjects({ + bucketId: req.params.bucketId, + prefix: req.query.prefix ? req.query.prefix.toString() : "", + delimiter: req.query.delimiter ? req.query.delimiter.toString() : "", + pageToken: req.query.pageToken ? req.query.pageToken.toString() : undefined, + maxResults: req.query.maxResults ? +req.query.maxResults.toString() : undefined, + authorization: req.header("authorization"), + }); + } catch (err) { + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; } - const delimiter = req.query.delimiter ? req.query.delimiter.toString() : "/"; - const pageToken = req.query.pageToken ? req.query.pageToken.toString() : undefined; - const prefix = req.query.prefix ? req.query.prefix.toString() : ""; - - const listResult = storageLayer.listItems( - req.params.bucketId, - prefix, - delimiter, - pageToken, - maxRes - ); - - res.json(listResult); + return res.status(200).json({ + kind: "storage#objects", + nextPageToken: listResponse.nextPageToken, + prefixes: listResponse.prefixes, + items: listResponse.items?.map((item) => new CloudStorageObjectMetadata(item)), + }); }); - gcloudStorageAPI.delete("/b/:bucketId/o/:objectId", (req, res) => { - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if (!md) { - res.sendStatus(404); - return; - } - - storageLayer.deleteFile(req.params.bucketId, req.params.objectId); - res.status(200).send(); - }); + gcloudStorageAPI.delete( + ["/b/:bucketId/o/:objectId", "/storage/v1/b/:bucketId/o/:objectId"], + async (req, res) => { + try { + await adminStorageLayer.deleteObject({ + bucketId: req.params.bucketId, + decodedObjectId: req.params.objectId, + }); + } catch (err) { + if (err instanceof NotFoundError) { + return sendObjectNotFound(req, res); + } + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; + } + return res.sendStatus(204); + }, + ); gcloudStorageAPI.put("/upload/storage/v1/b/:bucketId/o", async (req, res) => { if (!req.query.upload_id) { @@ -110,169 +188,242 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { } const uploadId = req.query.upload_id.toString(); - - const bufs: Buffer[] = []; - req.on("data", (data) => { - bufs.push(data); - }); - - await new Promise((resolve) => { - req.on("end", () => { - req.body = Buffer.concat(bufs); - resolve(); - }); - }); - - let upload = storageLayer.uploadBytes(uploadId, req.body); - - if (!upload) { - res.sendStatus(400); - return; + let upload: Upload; + try { + uploadService.continueResumableUpload(uploadId, await reqBodyToBuffer(req)); + upload = uploadService.finalizeResumableUpload(uploadId); + } catch (err) { + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } else if (err instanceof UploadNotActiveError) { + return res.sendStatus(400); + } + throw err; } - const finalizedUpload = storageLayer.finalizeUpload(uploadId); - if (!finalizedUpload) { - res.sendStatus(400); - return; + let metadata: StoredFileMetadata; + try { + metadata = await adminStorageLayer.uploadObject(upload); + } catch (err) { + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; } - upload = finalizedUpload.upload; - res.status(200).json(new CloudStorageObjectMetadata(finalizedUpload.file.metadata)).send(); + return res.json(new CloudStorageObjectMetadata(metadata)); }); - gcloudStorageAPI.post("/b/:bucketId/o/:objectId/acl", (req, res) => { + gcloudStorageAPI.post("/b/:bucketId/o/:objectId/acl", async (req, res) => { // TODO(abehaskins) Link to a doc with more info EmulatorLogger.forEmulator(Emulators.STORAGE).log( "WARN_ONCE", - "Cloud Storage ACLs are not supported in the Storage Emulator. All related methods will succeed, but have no effect." + "Cloud Storage ACLs are not supported in the Storage Emulator. All related methods will succeed, but have no effect.", ); - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if (!md) { - res.sendStatus(404); - return; + let getObjectResponse: GetObjectResponse; + try { + getObjectResponse = await adminStorageLayer.getObject({ + bucketId: req.params.bucketId, + decodedObjectId: req.params.objectId, + }); + } catch (err) { + if (err instanceof NotFoundError) { + return sendObjectNotFound(req, res); + } + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; } - + const { metadata } = getObjectResponse; // We do an empty update to step metageneration forward; - md.update({}); - - res - .json({ - kind: "storage#objectAccessControl", - object: md.name, - id: `${req.params.bucketId}/${md.name}/${md.generation}/allUsers`, - selfLink: `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/storage/v1/b/${md.bucket}/o/${encodeURIComponent(md.name)}/acl/allUsers`, - bucket: md.bucket, - entity: req.body.entity, - role: req.body.role, - etag: "someEtag", - generation: md.generation.toString(), - } as CloudStorageObjectAccessControlMetadata) - .status(200); + metadata.update({}); + const selfLink = EmulatorRegistry.url(Emulators.STORAGE); + selfLink.pathname = `/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent( + metadata.name, + )}/acl/allUsers`; + return res.json({ + kind: "storage#objectAccessControl", + object: metadata.name, + id: `${req.params.bucketId}/${metadata.name}/${metadata.generation}/allUsers`, + selfLink: selfLink.toString(), + bucket: metadata.bucket, + entity: req.body.entity, + role: req.body.role, + etag: "someEtag", + generation: metadata.generation.toString(), + } as CloudStorageObjectAccessControlMetadata); }); - gcloudStorageAPI.post("/upload/storage/v1/b/:bucketId/o", (req, res) => { - if (!req.query.name) { - res.sendStatus(400); - return; - } - let name = req.query.name.toString(); - - if (name.startsWith("/")) { - name = name.slice(1); - } - - const contentType = req.header("content-type") || req.header("x-upload-content-type"); - - if (!contentType) { - res.sendStatus(400); - return; - } - - if (req.query.uploadType == "resumable") { - const upload = storageLayer.startUpload(req.params.bucketId, name, contentType, req.body); - const emulatorInfo = EmulatorRegistry.getInfo(Emulators.STORAGE); + gcloudStorageAPI.post("/upload/storage/v1/b/:bucketId/o", async (req, res) => { + const uploadType = req.query.uploadType || req.header("X-Goog-Upload-Protocol"); - if (emulatorInfo == undefined) { - res.sendStatus(500); + // Resumable upload protocol. + if (uploadType === "resumable") { + const name = getIncomingFileNameFromRequest(req.query, req.body); + if (name === undefined) { + res.sendStatus(400); return; } + const contentType = req.header("x-upload-content-type"); + const upload = uploadService.startResumableUpload({ + bucketId: req.params.bucketId, + objectId: name, + metadata: { contentType, ...req.body }, + authorization: req.header("authorization"), + }); - const { host, port } = emulatorInfo; - const uploadUrl = `http://${host}:${port}/upload/storage/v1/b/${upload.bucketId}/o?name=${upload.fileLocation}&uploadType=resumable&upload_id=${upload.uploadId}`; - res.header("location", uploadUrl).status(200).send(); - return; + const uploadUrl = EmulatorRegistry.url(Emulators.STORAGE, req); + uploadUrl.pathname = `/upload/storage/v1/b/${req.params.bucketId}/o`; + uploadUrl.searchParams.set("name", name); + uploadUrl.searchParams.set("uploadType", "resumable"); + uploadUrl.searchParams.set("upload_id", upload.id); + return res.header("location", uploadUrl.toString()).sendStatus(200); } - if (!contentType.startsWith("multipart/related")) { - res.sendStatus(400); - return; + async function finalizeOneShotUpload(upload: Upload) { + let metadata: StoredFileMetadata; + try { + metadata = await adminStorageLayer.uploadObject(upload); + } catch (err) { + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; + } + return res.status(200).json(new CloudStorageObjectMetadata(metadata)); } - const boundary = `--${contentType.split("boundary=")[1]}`; - const bodyString = req.body.toString(); - - const bodyStringParts = bodyString.split(boundary).filter((v: string) => v); - - const metadataString = bodyStringParts[0].split(/\r?\n/)[3]; - const blobParts = bodyStringParts[1].split(/\r?\n/); - const blobContentTypeString = blobParts[1]; + // Multipart upload protocol. + if (uploadType === "multipart") { + const contentTypeHeader = req.header("content-type") || req.header("x-upload-content-type"); + const contentType = req.header("x-upload-content-type"); + if (!contentTypeHeader) { + return res.sendStatus(400); + } + let metadataRaw: string; + let dataRaw: Buffer; + try { + ({ metadataRaw, dataRaw } = parseObjectUploadMultipartRequest( + contentTypeHeader, + await reqBodyToBuffer(req), + )); + } catch (err) { + if (err instanceof Error) { + return res.status(400).json({ + error: { + code: 400, + message: err.message, + }, + }); + } + throw err; + } - if (!blobContentTypeString || !blobContentTypeString.startsWith("Content-Type: ")) { - res.sendStatus(400); - return; + const name = getIncomingFileNameFromRequest(req.query, JSON.parse(metadataRaw)); + if (name === undefined) { + res.sendStatus(400); + return; + } + const upload = uploadService.multipartUpload({ + bucketId: req.params.bucketId, + objectId: name, + metadata: { contentType, ...JSON.parse(metadataRaw) }, + dataRaw: dataRaw, + authorization: req.header("authorization"), + }); + return await finalizeOneShotUpload(upload); } - const blobContentType = blobContentTypeString.slice("Content-Type: ".length); - const bodyBuffer = req.body as Buffer; - - const metadataSegment = `${boundary}${bodyString.split(boundary)[1]}`; - const dataSegment = `${boundary}${bodyString.split(boundary).slice(2)[0]}`; - const dataSegmentHeader = (dataSegment.match(/.+Content-Type:.+?\r?\n\r?\n/s) || [])[0]; - - if (!dataSegmentHeader) { + // Default to media (data-only) upload protocol. + const name = req.query.name; + if (!name) { res.sendStatus(400); - return; } - const bufferOffset = metadataSegment.length + dataSegmentHeader.length; - - const blobBytes = Buffer.from(bodyBuffer.slice(bufferOffset, -`\r\n${boundary}--`.length)); - - const metadata = storageLayer.oneShotUpload( - req.params.bucketId, - name, - blobContentType, - JSON.parse(metadataString), - blobBytes - ); + const upload = uploadService.mediaUpload({ + bucketId: req.params.bucketId, + objectId: name!.toString(), + dataRaw: await reqBodyToBuffer(req), + authorization: req.header("authorization"), + }); + return await finalizeOneShotUpload(upload); + }); - if (!metadata) { - res.sendStatus(400); - return; + gcloudStorageAPI.get("/:bucketId/:objectId(**)", async (req, res) => { + let getObjectResponse: GetObjectResponse; + try { + getObjectResponse = await adminStorageLayer.getObject({ + bucketId: req.params.bucketId, + decodedObjectId: req.params.objectId, + }); + } catch (err) { + if (err instanceof NotFoundError) { + return sendObjectNotFound(req, res); + } + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; } - - res.status(200).json(new CloudStorageObjectMetadata(metadata)).send(); - return; + return sendFileBytes(getObjectResponse.metadata, getObjectResponse.data, req, res); }); - gcloudStorageAPI.get("/:bucketId/:objectId(**)", (req, res) => { - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if (!md) { - res.sendStatus(404); - return; - } + gcloudStorageAPI.post( + "/b/:bucketId/o/:objectId/:method(rewriteTo|copyTo)/b/:destBucketId/o/:destObjectId", + (req, res, next) => { + if (req.params.method === "rewriteTo" && req.query.rewriteToken) { + // Don't yet support multi-request copying + return next(); + } + let metadata: StoredFileMetadata; + try { + metadata = adminStorageLayer.copyObject({ + sourceBucket: req.params.bucketId, + sourceObject: req.params.objectId, + destinationBucket: req.params.destBucketId, + destinationObject: req.params.destObjectId, + incomingMetadata: req.body, + // TODO(tonyjhuang): Until we have a way of validating OAuth tokens passed by + // the GCS sdk or gcloud tool, we must assume all requests have valid admin creds. + // authorization: req.header("authorization") + authorization: "Bearer owner", + }); + } catch (err) { + if (err instanceof NotFoundError) { + return sendObjectNotFound(req, res); + } + if (err instanceof ForbiddenError) { + return res.sendStatus(403); + } + throw err; + } - return sendFileBytes(md, storageLayer, req, res); - }); + const resource = new CloudStorageObjectMetadata(metadata); + + res.status(200); + if (req.params.method === "copyTo") { + // See https://cloud.google.com/storage/docs/json_api/v1/objects/copy#response + return res.json(resource); + } else if (req.params.method === "rewriteTo") { + // See https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite#response + return res.json({ + kind: "storage#rewriteResponse", + totalBytesRewritten: String(metadata.size), + objectSize: String(metadata.size), + done: true, + resource, + }); + } else { + return next(); + } + }, + ); gcloudStorageAPI.all("/**", (req, res) => { if (process.env.STORAGE_EMULATOR_DEBUG) { console.table(req.headers); console.log(req.method, req.url); - res.json("endpoint not implemented"); + res.status(501).json("endpoint not implemented"); } else { res.sendStatus(501); } @@ -281,41 +432,33 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router { return gcloudStorageAPI; } -function sendFileBytes( - md: StoredFileMetadata, - storageLayer: StorageLayer, - req: Request, - res: Response -) { - let data = storageLayer.getBytes(req.params.bucketId, req.params.objectId); - if (!data) { - res.sendStatus(404); - return; - } - - const isGZipped = md.contentEncoding == "gzip"; - if (isGZipped) { - data = gunzipSync(data); - } - - res.setHeader("Accept-Ranges", "bytes"); - res.setHeader("Content-Type", md.contentType); - res.setHeader("Content-Disposition", md.contentDisposition); - res.setHeader("Content-Encoding", "identity"); - - const byteRange = [...(req.header("range") || "").split("bytes="), "", ""]; - - const [rangeStart, rangeEnd] = byteRange[1].split("-"); - - if (rangeStart) { - const range = { - start: parseInt(rangeStart), - end: rangeEnd ? parseInt(rangeEnd) : data.byteLength, - }; - res.setHeader("Content-Range", `bytes ${range.start}-${range.end - 1}/${data.byteLength}`); - res.status(206).end(data.slice(range.start, range.end)); +/** Sends 404 matching API */ +function sendObjectNotFound(req: Request, res: Response): void { + res.status(404); + const message = `No such object: ${req.params.bucketId}/${req.params.objectId}`; + if (req.method === "GET" && req.query.alt === "media") { + res.send(message); } else { - res.end(data); + res.json({ + error: { + code: 404, + message, + errors: [ + { + message, + domain: "global", + reason: "notFound", + }, + ], + }, + }); } - return; +} + +function getIncomingFileNameFromRequest( + query: Query, + metadata: IncomingMetadata, +): string | undefined { + const name = query?.name?.toString() || metadata?.name; + return name?.startsWith("/") ? name.slice(1) : name; } diff --git a/src/emulator/storage/apis/shared.ts b/src/emulator/storage/apis/shared.ts new file mode 100644 index 00000000000..fa1cd808531 --- /dev/null +++ b/src/emulator/storage/apis/shared.ts @@ -0,0 +1,64 @@ +import { gunzipSync } from "zlib"; +import { StoredFileMetadata } from "../metadata"; +import { Request, Response } from "express"; +import { crc32cToString } from "../crc"; +import { encodeRFC5987 } from "../rfc"; + +/** Populates an object media GET Express response. */ +export function sendFileBytes( + md: StoredFileMetadata, + data: Buffer, + req: Request, + res: Response, +): void { + let didGunzip = false; + if (md.contentEncoding === "gzip") { + const acceptEncoding = req.header("accept-encoding") || ""; + const shouldGunzip = !acceptEncoding.includes("gzip"); + if (shouldGunzip) { + data = gunzipSync(data); + didGunzip = true; + } + } + res.setHeader("Accept-Ranges", "bytes"); + res.setHeader("Content-Type", md.contentType || "application/octet-stream"); + + // remove the folder name from the downloaded file name + const fileName = md.name.split("/").pop(); + res.setHeader( + "Content-Disposition", + `${md.contentDisposition || "attachment"}; filename*=${encodeRFC5987(fileName!)}`, + ); + if (didGunzip) { + // Set to mirror server behavior and supress express's "content-length" header. + res.setHeader("Transfer-Encoding", "chunked"); + } else { + // Don't populate Content-Encoding if decompressed, see + // https://cloud.google.com/storage/docs/transcoding#decompressive_transcoding. + res.setHeader("Content-Encoding", md.contentEncoding || ""); + } + res.setHeader("ETag", md.etag); + res.setHeader("Cache-Control", md.cacheControl || ""); + res.setHeader("x-goog-generation", `${md.generation}`); + res.setHeader("x-goog-metadatageneration", `${md.metageneration}`); + res.setHeader("x-goog-storage-class", md.storageClass); + res.setHeader("x-goog-hash", `crc32c=${crc32cToString(md.crc32c)},md5=${md.md5Hash}`); + + // Content Range headers should be respected only if data was not decompressed, see + // https://cloud.google.com/storage/docs/transcoding#range. + const shouldRespectContentRange = !didGunzip; + if (shouldRespectContentRange) { + const byteRange = req.range(data.byteLength, { combine: true }); + if (Array.isArray(byteRange) && byteRange.type === "bytes" && byteRange.length > 0) { + const range = byteRange[0]; + res.setHeader( + "Content-Range", + `${byteRange.type} ${range.start}-${range.end}/${data.byteLength}`, + ); + // Byte range requests are inclusive for start and end + res.status(206).end(data.slice(range.start, range.end + 1)); + return; + } + } + res.end(data); +} diff --git a/src/emulator/storage/cloudFunctions.ts b/src/emulator/storage/cloudFunctions.ts index 8c914b0e9ea..0c4a0482dd9 100644 --- a/src/emulator/storage/cloudFunctions.ts +++ b/src/emulator/storage/cloudFunctions.ts @@ -1,12 +1,12 @@ import * as uuid from "uuid"; import { EmulatorRegistry } from "../registry"; -import { EmulatorInfo, Emulators } from "../types"; +import { Emulators } from "../types"; import { EmulatorLogger } from "../emulatorLogger"; import { CloudStorageObjectMetadata, toSerializedDate } from "./metadata"; import { Client } from "../../apiv2"; import { StorageObjectData } from "@google/events/cloud/storage/v1/StorageObjectData"; -import { CloudEvent, LegacyEvent } from "../events/types"; +import { CloudEvent } from "../events/types"; type StorageCloudFunctionAction = "finalize" | "metadataUpdate" | "delete" | "archive"; const STORAGE_V2_ACTION_MAP: Record = { @@ -18,29 +18,21 @@ const STORAGE_V2_ACTION_MAP: Record = { export class StorageCloudFunctions { private logger = EmulatorLogger.forEmulator(Emulators.STORAGE); - private functionsEmulatorInfo?: EmulatorInfo; - private multicastOrigin = ""; private multicastPath = ""; private enabled = false; private client?: Client; constructor(private projectId: string) { - const functionsEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); - - if (functionsEmulator) { + if (EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { this.enabled = true; - this.functionsEmulatorInfo = functionsEmulator.getInfo(); - this.multicastOrigin = `http://${EmulatorRegistry.getInfoHostString( - this.functionsEmulatorInfo - )}`; this.multicastPath = `/functions/projects/${projectId}/trigger_multicast`; - this.client = new Client({ urlPrefix: this.multicastOrigin, auth: false }); + this.client = EmulatorRegistry.client(Emulators.FUNCTIONS); } } public async dispatch( action: StorageCloudFunctionAction, - object: CloudStorageObjectMetadata + object: CloudStorageObjectMetadata, ): Promise { if (!this.enabled) { return; @@ -62,12 +54,12 @@ export class StorageCloudFunctions { cloudEventBody, { headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" }, - } + }, ); if (cloudEventRes.status !== 200) { errStatus.push(cloudEventRes.status); } - } catch (e) { + } catch (e: any) { err = e as Error; } @@ -75,7 +67,7 @@ export class StorageCloudFunctions { this.logger.logLabeled( "WARN", "functions", - `Firebase Storage function was not triggered due to emulation error. Please file a bug.` + `Firebase Storage function was not triggered due to emulation error. Please file a bug.`, ); } } @@ -83,7 +75,7 @@ export class StorageCloudFunctions { /** Legacy Google Events type */ private createLegacyEventRequestBody( action: StorageCloudFunctionAction, - objectMetadataPayload: ObjectMetadataPayload + objectMetadataPayload: ObjectMetadataPayload, ) { const timestamp = new Date(); return { @@ -102,19 +94,19 @@ export class StorageCloudFunctions { /** Modern CloudEvents type */ private createCloudEventRequestBody( action: StorageCloudFunctionAction, - objectMetadataPayload: ObjectMetadataPayload + objectMetadataPayload: ObjectMetadataPayload, ): CloudEvent { const ceAction = STORAGE_V2_ACTION_MAP[action]; if (!ceAction) { throw new Error("Action is not defined as a CloudEvents action"); } - const data = (objectMetadataPayload as unknown) as StorageObjectData; + const data = objectMetadataPayload as unknown as StorageObjectData; let time = new Date().toISOString(); if (data.updated) { time = typeof data.updated === "string" ? data.updated : data.updated.toISOString(); } return { - specversion: "1", + specversion: "1.0", id: uuid.v4(), type: `google.cloud.storage.object.v1.${ceAction}`, source: `//storage.googleapis.com/projects/_/buckets/${objectMetadataPayload.bucket}/objects/${objectMetadataPayload.name}`, @@ -237,7 +229,7 @@ export interface ObjectMetadataPayload { team?: string; }; etag?: string; - } + }, ]; owner?: { @@ -263,9 +255,9 @@ export interface ObjectMetadataPayload { * Customer-supplied encryption key. * * This object contains the following properties: - * * `encryptionAlgorithm` (`string|undefined`): The encryption algorithm that + * `encryptionAlgorithm` (`string|undefined`): The encryption algorithm that * was used. Always contains the value `AES256`. - * * `keySha256` (`string|undefined`): An RFC 4648 base64-encoded string of the + * `keySha256` (`string|undefined`): An RFC 4648 base64-encoded string of the * SHA256 hash of your encryption key. You can use this SHA256 hash to * uniquely identify the AES-256 encryption key required to decrypt the * object, which you must store securely. diff --git a/src/test/emulators/storage/crc-buffer-cases.json b/src/emulator/storage/crc-buffer-cases.json similarity index 100% rename from src/test/emulators/storage/crc-buffer-cases.json rename to src/emulator/storage/crc-buffer-cases.json diff --git a/src/test/emulators/storage/crc-string-cases.json b/src/emulator/storage/crc-string-cases.json similarity index 100% rename from src/test/emulators/storage/crc-string-cases.json rename to src/emulator/storage/crc-string-cases.json diff --git a/src/emulator/storage/crc-to-string-cases.json b/src/emulator/storage/crc-to-string-cases.json new file mode 100644 index 00000000000..35f16337528 --- /dev/null +++ b/src/emulator/storage/crc-to-string-cases.json @@ -0,0 +1,52 @@ +{ + "cases": [ + { + "input": "urHxOuppeVbNrzXoWCTHDnR2ArEI31n8h6RpqFyiKfsxACyvZxJtSW7njWmXh0taOaBWLXIhE2nLQ3LlEdR3Frewnw3V6OgZQHhTAAAYC8yPXzVF9fjOXK0YbkBntZy4DMhTxO56Qvyjyoc3Sx0vyYFRZVzAaO4TCyE0TLaYWjVrZSmlqU3lH8tKwhYoIDGNuXA8C4m4FoPmmTTDaMpykJ4XHpX9iTuo86RnkJILYAJMiiRvlc1GUnCnwfeX2wtI4zRMqoouixCFxnfQPZOrytIOi2eSp3Tei43MIor7fDqCnH51OmnXacxy64vP3zfSDmf5PSbBDFVH4dpYymJIJDigKewHK5HD9U968J3BaiJuycX7dTAqJ2L0KaKMOzchh7C5fdH4rBQ67RNlbGq02zSK2ghjLmhb8Ei3UP63c8vXrSlxBtKNGAL2zcmI7xBPhW3Yc1L2D79qUvR3ADR2tqRia3nnKccla3WxKzUldileTjXdDDrRttBUdAFXDR2Jn7rF9SH1HHVVLqNt2w7N0i00u5TwlgXsZHvDBpRYEWeUMrtfehm3TnHzy9n9qdlQzThuVeUSTw0r6paSS6A1i9nlAsUK5wboero9FZ4Bb6A97jwtiWsjfKjB08CEsPuyZZznhiODPrFUD4ONwv0IXHQCMma0H48JBsRQmffW5ukywpVjxfjO5Xq1ycI3Zr4yT6g0YcPGnBeZsttTk7NsIy1ImwgP1gTdeyZVsJ7nUdOk1bHb4bSvqnKUj4kiiowCukmng2e6TtAE6hwapknBOKJbIGEV5HaRKG3tZUNhB2U0X3TIEh3EI0wW5A5jzXHtLrlEEpYfmhsgM8Bb6yewkxmY3cnxOwAeh5NvjZ3PYN84IAhITmzcxV0prYDwPwmfpkUPcJE8H9lcDHl8iyKCeeJicPgHShrrb4XzAnm6ZbFw6IIMQVq4A9Yq2vgwiPxsSwewzoNswyKVOABRW3weIJV9KPBWKfWZIzOcttmimP8w9h2yVjB0PGQm7pYNR2wq", + "want": "XO7ASg==" + }, + { + "input": "Yy0Nbx68n0pp7Xj8w1VzOmRQckE62O8aNZbWE9fWzghcO0LtdMxUHsdoMEW4WVzVpeRAe4Sq3OKqzjAFL41EsEPH1giGEcAkTS9RawQW373Q03nOBh1AaLshefH5DswejRaUkd2VCdskTKaViqJqKdNtX1SWvzXkSfmsjnwNpDiwnmNBlQ6gZ2h8oxyPrhvh3JyXY6dQiUzG43i4D8RgLiA1ZOKHSYLeV73UlB7MChfYvKocvKFdRzXRuM3UptZ4qOR8oOxFSMs5ffKtpCrsX2ILtSNjJGUvv4aDHSZoP8xSUsgZqv8GnJM75mC053JtrhAHlajx3Fv4CZT3jgAWomH20koxoJVBp1SzSfHz6WgfJETLt7O132I2X00UUQ6KHoUe3Wf1zCK3UzBVoXRMNsnSLjqOTCncb9akn1QggKgL8ZdbgrvY9xcAUdEj2yhK4Y0HiUulTMmmlcOn6qODeHmEyAnHO6fuovlzffsuKCEFEKZHie69Rd4PumHBdkVQIGmpZEb70aadgoVbE3oPm3xsXd3jqDF6bUxFzxqoH4hdreT6xXKQhy5NKEEzctnAoCb1JZloKK3lncwsiGF2z37pPEiWkkesGiDEo7CdsrNYllMjLv6I4BW68ldbA162Mpg79MXV9kajdJmJWnkGQ1tVjsX0B61fUWvzuXjwwoxORhpmo0wXYr9b8Ap6UYFwU3qYWgoZC2vXRi9HUXTv5kyFytRAqqFJMy1yVan0AQSubTOWWLLWFKZfuWbwa7d3KmPuvFqSl2WqBMgSnnqcNveyxlQ2PgP5NAel9wuQqafasNsJTUnoLW1HOHfNmAlLvBhQGFcKNBaKk5jtAtu1yGBS1JU98JulkdvI5ThEqeNufrDplxjsZOZdkddZovCnKGEMN2PptfxO06AK6CkIpKhsAotpWwWqHuDK01a3jrOTurhyCiuJBv2z7LwD8DaRITQKEdaahDbsLK2EE2cPXl5sW5sfQDVR4nbOM6tdaGQ6ygtedHUQ8mO5ypD9g0N1", + "want": "eRzepg==" + }, + { + "input": "4XuSfnJ2BtYEXL2roCZeb4J44tqMzJyWPGrKZ3ba9aCn6v93jqD4aRZw420wElZgtJbdJRWHQMQIQh4fCvHSSP2fXPbulgFAOhivhr7v06jtiLsWJrxLfLbURumD9ke44mabgcKRTkzZDeHNuYxEKK5WxqEbyC1Y8t6S82qXS2vwGowr3AkoKEXoIrRsRkzuWpjGIdW8dOHMFek268yOmZm9QSwvxMr9PXzdiFXs73QzYoPrBKrLqYyskIcQ5hglBUif9M7u40k715HXoSx0uJjCURRWcJYOESoTODgaHfmAJnr9mG0wiVeUXlwtRUZWLoCBvZWLQs9keaXWBfpBy02UXSgtlOkiPfOPp8LL0lMQn9IkzEuHwMEnwVX2nRwOtqbX3ZvXhggwh60dfIU3DmhlRuIFB4SCBNvT2XvgM9YGkjulLx8fh3gkQ13iwuyyTfrH8vWJ0SHz4stp43l6QdchDzPsuWDH7cUplhx4uUHIHgRkuXv8QTF0QPQ9pnlE30T6ZKKy28nHoaD2iTqysU8Dd2MCxhdWznpyZ1eGCHeVv7tzd0Kx3VxUXr296Vam0IA4jgpZopu0MWY1gD1OoY0GJ7RkfGELrGAjDEegIr7fDa6SWMDRJHZLsvciJc4ONlNza9vnHQlpXgIlS7nP5Mnqhei2wvH3j3yxWDKFn1ZBKl5waEaD8Gh3JXeleu0J9sK39CzSsBymYCJG9kgK9BAW8HVldJuLpfoDoI69QqBPWiGqG9nXk1bV9S0aJcXBhEhCEfAQCpkXoWTbai3SBAdPySO9jLJ6IP3srAOs3k9E14jzivo3Z9cv2pKXTpGt1Ey1FjFfZaYPcgleK59VA9xJPCP07hoBo4X7lZveqnTHBlAzx4cSTn8xXHmVwwqXGI699LTAoxB2Hmaf12YvHFpmWX1xw8DFriHrOVKryjDmKgaMrDJqJ4CoQHhhdjOccAK7W1p4cVTXklvbYf1R0Y9gG5nB7ikPPX8gkTvUfmTM5JGWkC1IL34VZUJLz1NY", + "want": "SedZAQ==" + }, + { + "input": "sAoTUmuy0HvuzQyt66tTFQUcuy2YFalWPA6zQ8vKcCCUBXm8W833DPazZ0koBlPovq52hiFY552UxVYKhHu5IgoBlWfZRe7ZH6NOKU09KnvAzZJu1uhjo2llTlgliBQFPJIYJO9OwvzwnQ5Q1kc2pFnvKrR6KYspE43vVAKXApuDOs6DvXEdGvWoGPNUQJQPD4FpapEDSy10nl12OWt459ITBdjcDwBEwswOwCsAjh77Zslynyu4ieXftOeZ04xCdW9wShRD96ACL9suX7iAWQxRawoBlRwXMUOwiD8TAOomr7PYrPmanH4jPfZz4vIPbHvsb2srGjUbXYidxStuBnDsNLLkvM9AVicKSL2mCorqTP7sdkSFBdON6JwcNMQzv9rzvDXHpnidWHP6gmfd6hcyUAmzLMYXtlEP9V1thb7moCu6HW9Latl24QaQYSplMkQsfRFrW0PXobHbGvZV9CamvyAl7Q8c9GnhKKj5f5wlUHr3bmUe3rQLcmv6zL5wA1XcR8nFW5DgIPulFJry6lPtL3o404zj1cqPiDfWtd7j5FrZ5I7WC77DqX96eD8XT8RKlQnMxCdLgCYtPSxJM3eU9dT8MRF4CQkEHGvKh800cnao8cDml8rlpYh3nB0lvdTgQmW17uVm1fVKR4mwceB4c23hCsi2kl6z0eBw2OMBv3e6Yt39Usl5XQj4JaRiU4Pz3ttywouze2ZitujkfZ4PoIJ67gmLV3QgIT6g8oP0eA95G3cvuBYBXOjrqPgWYJjG9UCs7Pn13XJTILfPskw8pKSIVcjzQAQwfrOsS7x00I8DW6AevM7sGzaS8FbY3q38gSuJfvospUqvWlI1ZKjR1esv1WzfPIu5Fh2wmNGmKprsL4u66LYX8Bt9KRbdjYgvT0L1hJPvQH9heGWVnE876EHuTodFrfgqIc264PKZNSt9RJGD9Ztad3Oi84YOF7f6UOIZ5EuNHfpnHUsjDBVIhC5NimOesm5aEIWn4HEWIHq4OZo8GuuzXj6p50I3", + "want": "u8v1Fg==" + }, + { + "input": "8Z9xr4oFu2btq7QKA88udFR24uQputf0rbehcNN9mPOFldJCXjb4UAme7220x7sFlFUAxo9Jl2x68cmAyQYB1XyeAmNso8dIJp5wDG31sDMxZItADFoOM2LBpeN13AqokwoRFGstmVeQojBdk22bO1O4qK5ysoMwhX8rVZBdNneC9LNqKXVV4yNVGYENEuOSecpnE8oYZgljl15jhD97VErppsLogD9Gp7t13G3QvI7utr61wGcrEdvJZScWE5ufOOyHVuJVVu3Nr6g2BTj5jwr4RjShnmZAtXnPZo23aaONe4swFNKZnE09r93SIxlomEXGEx9Kh60Utqhy49qRnehAl5dhB1MBMVtLrgW9yi8yjEuhhwaRU7WSn5TvOjJ0UeCzpgxmPIccJSoPfv4vzWrQVgsNWu8a2xdtvPagrU0LWXpHHxUobvKLgbKDWzCJiGzUK2tLkGSo5RMnQlrZ1zDfj5jC4P9b3OcRlk0HwCjc06CzSIj5uOpIDXoYJqCzC0hy9v5tSJd7biWdUkoDgJ6RfEHWdOhFVka3zZalsVheLiXzR1p7wFxxfxpKhbJvK5j12JML6BLFN6qY7wO0jkzZWoZ7IQETkYGib4lmheVvQkxGh9SgpdvcgZBW3ZCAxcOD1snXkcSh8V5OgkRxoWOhmMMR9Hwd4cHyjbvrGuapPTqLqicECvDaauvZ6avrmvvDnQ5Gmll5zJcFvMJo7r6scRca1iduaXaMSfsz2Ki2jeGJ0YRx35w0umxyUfn4BEEcGMPLRUePPUjMl3tmjhQ5KMfrKHBWGA6RXNZxtgGy4twAU4bJWCNPx0szCEMOrmcU2MvpBdz45SxCD11RRnvkb3Aq5S9uoNYX8qHhfaD3Zlez0fSvcAkQbICXLwKZPIKgfDdUzGmqyYZlYMLuMHNeoedp11bwBrFMixF9B6pgoV6ejdpOtV08U6gR5XUpqasTSp2gwPMUJrvRo9apn18tBo3OPK5v0noBV851lI21Qm9OACzKuTqmBV40jl7L", + "want": "G0cB5Q==" + }, + { + "input": "mcJLQJ9StJpIPchIF76VOrgbo8hPnUlL1ARpfEd6YPVN3MWmEtH73LUlBtNpgtDWia7VywWnxiOaGlSOXmtGGV7Tp24ij4sCZrpTmJzYI2qOpvUnIV1BUUsa76h3e4UAfZUnBb04MJbxa0TJUY55R1wtcp7Qmi7Hjg7NMuMEWSHOD2nwg8LIzxNZf2ejqSd5rQC8AwZQ4xEn1S0yD2NrEKL5yg1q3zlN8btzhcbiiyAYFXu7bdhNXjVe9EsiSTkt06hGvIwrPbbFb87rf5C3lM9QaMsjX8P93SiPKW8jetPe4bhLiTcvHnI1QRgbyMMNzryO9mLQn0GYzWYUuXWNpqi3aVbKCTtfWPRFJnMBAyGUM4oNAmh8u9H6y0YrG3D3PAIwkUsQ9XlPmbBAGoKLpNCT8VcaJIrrCpX6geahpnHVmqSANDgDucL5O05olShYAYTAH18q2GrKB5hh1npIBZvb3rigY6304WNozdk3kc6AfK9FSi5xZUTX0nZoDfKJyzwCDoLamh8UTP9KXVLuXCXEVhpj74YOwUOt6jVZ437aKn53JpxFrymOfqzRI7QEOEVg0SibMv4fLD9IJ3crbA2slBmBdPDHXBzcEOsHmQ8Tmajw74gvPGEKwv8XuTtNSf4ZERJq5goCk0RdES2ISB52Ue9nUVzAmq0YrmMaCKt5OgVSgxnarjiTyt7zBm0IUAoTu4Wd91BMIksTj7pjGYINrkeNldFywmUmUxo5IwYYTitFPkkEMtIAh2kAkNJxuYvdWfhjgCMnnTVeGF46mY1uO3NZLGkiqxGEfGsdztnjMEvVcob0B4Mz5Oy0F8TIGcl7SYdf3R0eJNej52TX1A2HeUWjeg72w5d1XGKM6n8UvkYQ4IrGzMuE2k0VyvF1Hoqj1yONP7zlSIz3utFIq3RIU6YJtZg5oUvT874tEVlticIVennWrxIIhGDLI7MliH4LfhPmuFkQ7ykkk7ns4BWm6COAPpwinh1yQrd3ap0NE0GEZZZkG6lBlbmUfaIq", + "want": "FvSbcw==" + }, + { + "input": "aDd0IIdFLEXBS3dvaACLKwDg47dfjZ7XVmYhk9PPazz4ZQqNqgGUqSMO6Tu5EcQXhcdJVRrfYnrV3IcdS7GkldEf2sykdLGaM9HIVt4QFgtUMrAmJjcHKuvM5UBKC4Xyt7LkYWaWy1cga8y9PCwknnN4qf5RPFDlJuKtONL3rUyo6oSteqDJp8U3Y3kekIWDyAjsEv3ButBLjZXUDaHJ86rbB2H72PeTrMhube0h2ZQJu5R4CmCmtCTukvzzaLiExHkJqWnqwZmy8ofjtw5ur7qmH7lcTtsxGW2zyqmTeBstsGO2K64eUnArUN4jLgEqViYdgE2qI2RcHAWGThM9HKUjNgeRPlAFi1cNxM9DjqvOxTXpzTQSIjI6ExNf8GZSG7WSkcVCFedfoxK85iqm8xpJjos0WLEYAi9Cxp7Cnq3SjjVVOUrYN1ayNppdLpEGcewXkwpfXb5e8yTIxdTn9I0qurhVJb7aHdTnLEsCltVTdwir6ZxxxsdHzaUf5LyoYllFxsOtnycApNL6qG92x0iHymcVq1BF5ahGqHLSM6F0sGNaAW6paHuGG4LT5b7O5KrLznyXQVIiyu83ND63VsbVFJ5QDbunqiDjLSojHfWYwsoYqBNzgBWou4kTPcLtYn9oMPuSqX1GUtqrZI6yNm4KeAJsGXjPx14PxaKpmJ3Bzo40tiuh2KyugiGmkd4FYWd9AN404L7WcTLCjwTMEOCPql5MV7yLyvnfTsxETc9vAdSqUbnjdASeaxufnyXAAbtQXDV7IYUWkLf0ctjVT9PFGv7F5BBrAuJifXOS6bVOogVuPAQtlZl43lZM0p2PIOSYk3FdwlOD7PbiQJjZBVKPQgKx3fPE0h4Iy91Q7X6PTJ13kxzCYv62viu6sww7WWpLsCJeI5atCibSkd7JvU2BmjAqkm7u8CZ4gvMRVEI5bQKekVGxF4R6LfxipFlbW9nzV1B5RHoNH7E3EQ1IvkQX7MIs2487MONCprC1MDl1Om4IjQYp2SAdtVPDV7EW", + "want": "leJorA==" + }, + { + "input": "nxY0T0EDVQQiDtdmWQJCtCmIGq4EYxHN0lGi2Gpel7ub5BgLhILVzkBlWH5XNmU9ZkJCLB48kI8MHMMLatj5k1gw3aZpQ5zKPPMwO6LvC7XcBdY0yKxqjoteiyKCtdpb1AsjQ0LhHwivqmzsQ0yuvBt6N5nHdqGGhxXIx6cASSbzDNezHgsT0c86bQ7hFDHDkgxAsPoNYGLfrvYZRZ4Np4OckmvmU6pNzM1o6fy3NBn7gBunwFE3rO4M63hvaYHVZA4BdXJWwJEfWV7JpBXpx5nJI507sffgktktfdDQfSdqUvnSFGPbIf5LGYyvYn3XHIrIz09nXJSobVw7AKneURlHcUhCOF6N7hTxMtKNoEJgfI7XmlKnfANzi0DQn6h98W5iNGaZuDKbPqDsHU3tLWgpv5002tTEXIlznDeNVepibutb2s9TAl24JxwPa2uLWnWPvC7Ft8ikQAJtf6VACX4Dx2JOoJEddAtPFHMIuUA8CyxBtAq1qdrGNf2UZuEKDJqsWafndflTKbiT1XNQ4a2CU0RakvZ44pUmRpPE6a8MnhIOixK8hyOTRHg3MTmpSavAldv8qOWdFZ8ZQ20bw4YCsg9r7XRx9Mdudsij9S7IZYSpnQIXKirq0kZUbukvDi15hYofdkNTktNPsRiHHp89dYhCSG3ZBB84izvHlrTEGopTnZA7yBV98aDQkrKH81U1op5MbWzBk3iqAuSIrId4wOMEPoly0F1SeawWAnffeq4o4ryRxXO70TbNzVoBeirebrNTrFumi5GnqnZ91Tv0sKCWrFsp5ECmF3MA1KXXFQy2gEFG4CstPlaLyPibbeu2PIQkxY4PcZBP5KQLcAxzNJmya9SqxIGjruUPWDa3gS3s4xIgKHrtHf5J25YXwWXyYuyyIpym51iGEvB3psBBoUlBJUZlwrCN4TJCLGCYYtqzuDyIMAixs4whkysB9z2Ox42f36Np4J0th6mU9psUBl9eIUs0YDnwlmbnX52geUCuVSYHk3wZgWvKyRts", + "want": "NpkX1g==" + }, + { + "input": "R8RS0tQyhbUwJF7TcthrpaUfnEjeY4jMF4P0QsQKk1ksW4o29fKG1gL4QaVLatCy0pEgNh6ONK3OkyIlCpl5XETCfBMAR0877r1qFKZzgWYHHffkjicJ1gnx41d1JDUb0828QNbzAeR0stHK0KSRGEAtH5c3IKoDL8D1GrkEavc2kfET11S9STHb0P8PzxzcQ5CFnjp0oeU7HA0BE2mRzmKsFKmhB2lQ8FuGm7CUkpK0TY6A9XvZJByNoLMjFfodG9NB0KQ3Q3fIZICl4bso6jIPq4EAoKIk4PQwApfUKa6ILKvncvVHBqPUOLczjlunBIMv7zMuMTb0GrTsmYYKnR6Q1gbUa8u8EdDwxxrjWtNjNk8O9bRMA5L8TUOjA0HS4xp1nbYGzIukr6qyYFBNiPBfwhc5IGGDHmm4XmPCcXe0fFsvPQhLdXtLjdNIKObdjPyznZOokrQ7dVPVy18qSQEifsNi4FS9sC4hH4BHHjX2nJR0kHpMQJu5wVdr71XCHVa37FnJUQuPmQD1Dv05GSXtm0mgN3D3OLu14LDKQvQedSLOmBjSKRt4uUa3ddMeFsjUnTPJUMAyrF3g15uz2Xq486ZpqoQ9NdxhL12rkYLO2XrP3BqRTdIjaw8D31kJAvorHGyUUq480ObwagGG5jpsO3UBIffP1y42CPvyVK5XahtLnfr4R20PQbmukzzHsfVzEMWR11m4orax98MaLMvhxAJ4t0vZXKkTxsj1dGq0yaCamHiSYgh7i1I7oV5DwaDwe4JQWvY8YLOCC3MKjizClUs8rkRXxA5zfKn8AdlgY1JpQ6eZHCCUnXP6inHNTfRGFhDijXRYSvrP150Kd1ei3YVDjzfxoZqzndM2n8YuTm0N2kkKGwQ5BimST3Nehj0llvCZGpM7vUpSUKTSPMF09ndtmbnEnfyOZi0HHXyB6BQiTzcxIAGCxZ6iaK1xdyeUdkF4MW2KsrCZw7h9dWkwpRQR10YDzVJiVNAHlw94E8CosS3wSgGdUljqL7ol", + "want": "sPl74Q==" + }, + { + "input": "ptcSzWd5VCyP0D4RUZoBgPwzGHJAvah7YwCCPXOKGmy5kcHX4yYUQNnn1TOSj6GwSNrRDDrfrY3JvkxAZL2KgHglIRTJXg6L8ghv8d3S8Bcvgjey4PUv9Z0V2b0dOVeavoxaE1RxZcAGgxA3oPkbIfAwKZHRbmiNI7xpoJIyQMzH8iI4hPy1SK3v6etwSrUHV4CJoIZVKsMYRArBSXzPVsCrdax9Hi7wUAxXQsTSkXTWynnA2b1xVs9GzTKCta2bmSinyA3gaRmIhkpTTC0ZeCQuGFvSTMq5ya7EAlMzta7Q0YEM8a6nWEgVuRau69b1dRjDvIPYkPuHiYyy63QgdNMAdPxG41Yd8bbcJXmbK2CjVKiX24F0G9g2ywSjBN29Xa2QSZPYS0tk0P98apjQKziHjQfwnEcdxT8m46UG0AqvPdJwxZCjUQYEl6RQSp1djjTxcYKbAyrfP9xZaxR2QojiSd0CgvsYJHVCj5x5JwME5EWcDWMguQiyGgYdCmzUUuCxzlToj7RpB30U36lRCHW7rHj8MvwAcMOCqw2DMfRLQp51Ttaral82MUhDfhTls4KedmTqoE5benI9coxyHxzP4Coc8cEEU1SojnWnoBzuDDTnXDw09LqTTcXUn2soduEnGmDEIUNQoXiy5ylvgu5oz5Jvjv4GAcPCBAPfglsOzJDOcQ5F8LaEuV6JgPtfuMwfUH89rWKiGhSXxHZxTDJDAUPpmcQx0FFOL0KbobY8JdcsqJVyWSz2awFDLeBipYTx9U8kKdSwKltsXjqWAIPKoQuY0k6XdfIh65Bk6IoYw3Vmt86ao2kyj03w7fdwiSutb8RQ1CwzxdytUqcNyLscS3PesnGpNd76aRKoeKminQJENrEzTpc6tImLozHgvGG7iuO6eSwRfotS0IXj5czvzRktW8NEE1DQoTZcBM4Doo6bvPc6fvnjiLBpF3LBQzHlJaI2YLrV66axhsd6qzu9KlF1wRdHRdMzWjOLGIuDWtlZ75PDLtgMutLVuKUc", + "want": "+5vKQQ==" + }, + { + "input": "", + "want": "AAAAAA==" + }, + { + "input": "\u0000", + "want": "Un1TUQ==" + } + ] +} diff --git a/src/emulator/storage/crc.spec.ts b/src/emulator/storage/crc.spec.ts new file mode 100644 index 00000000000..97c7e88f3dc --- /dev/null +++ b/src/emulator/storage/crc.spec.ts @@ -0,0 +1,48 @@ +import { expect } from "chai"; +import { crc32c, crc32cToString } from "./crc"; + +/** + * Test cases adapated from: + * https://github.com/ashi009/node-fast-crc32c/blob/master/test/sets.json + */ +const stringTestCases: { + cases: { input: string; want: number }[]; +} = require("./crc-string-cases.json"); + +/** + * Test cases adapated from: + * https://github.com/ashi009/node-fast-crc32c/blob/master/test/sets.json + */ +const bufferTestCases: { + cases: { input: number[]; want: number }[]; +} = require("./crc-buffer-cases.json"); + +const toStringTestCases: { + cases: { input: string; want: string }[]; +} = require("./crc-to-string-cases.json"); + +describe("crc", () => { + it("correctly computes crc32c from a string", () => { + const cases = stringTestCases.cases; + for (const c of cases) { + expect(crc32c(Buffer.from(c.input))).to.equal(c.want); + } + }); + + it("correctly computes crc32c from bytes", () => { + const cases = bufferTestCases.cases; + for (const c of cases) { + expect(crc32c(Buffer.from(c.input))).to.equal(c.want); + } + }); + + it("correctly stringifies crc32c", () => { + const cases = toStringTestCases.cases; + for (const c of cases) { + const value = crc32c(Buffer.from(c.input)); + const result = crc32cToString(value); + + expect(result).to.equal(c.want); + } + }); +}); diff --git a/src/emulator/storage/crc.ts b/src/emulator/storage/crc.ts index deed2c336d0..08ab5c87e82 100644 --- a/src/emulator/storage/crc.ts +++ b/src/emulator/storage/crc.ts @@ -28,6 +28,8 @@ const CRC32C_TABLE = makeCRCTable(0x82f63b78); * Adapted from: * - https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation * - https://stackoverflow.com/a/18639999/324977 + * + * @returns CRC32C as an unsigned 32-bit integer */ export function crc32c(bytes: Buffer): number { let crc = 0 ^ -1; @@ -40,3 +42,18 @@ export function crc32c(bytes: Buffer): number { return (crc ^ -1) >>> 0; } + +/** + * Adapted from: + * - https://github.com/googleapis/nodejs-storage/blob/1d7d075b82fd24ea3c214bd304cefe4ba5d8be5c/src/crc32c.ts + */ +export function crc32cToString(crc32cValue: number | string): string { + const value = typeof crc32cValue === "string" ? Number.parseInt(crc32cValue) : crc32cValue; + + // `Buffer` objects are arrays of 8-bit unsigned integers + // Allocating 4 octets to write an unsigned CRC32C 32-bit integer + const buffer = Buffer.alloc(4); + buffer.writeUint32BE(value); + + return buffer.toString("base64"); +} diff --git a/src/emulator/storage/errors.ts b/src/emulator/storage/errors.ts new file mode 100644 index 00000000000..f1021ca7daa --- /dev/null +++ b/src/emulator/storage/errors.ts @@ -0,0 +1,5 @@ +/** Error that signals that a resource could not be found */ +export class NotFoundError extends Error {} + +/** Error that signals that a necessary permission was lacking. */ +export class ForbiddenError extends Error {} diff --git a/src/emulator/storage/files.spec.ts b/src/emulator/storage/files.spec.ts new file mode 100644 index 00000000000..062ce84f696 --- /dev/null +++ b/src/emulator/storage/files.spec.ts @@ -0,0 +1,187 @@ +import { expect } from "chai"; +import { tmpdir } from "os"; + +import { StoredFileMetadata } from "./metadata"; +import { StorageCloudFunctions } from "./cloudFunctions"; +import { StorageLayer } from "./files"; +import { ForbiddenError, NotFoundError } from "./errors"; +import { Persistence } from "./persistence"; +import { FirebaseRulesValidator } from "./rules/utils"; +import { UploadService } from "./upload"; +import { FakeEmulator } from "../testing/fakeEmulator"; +import { Emulators } from "../types"; +import { EmulatorRegistry } from "../registry"; + +const ALWAYS_TRUE_RULES_VALIDATOR = { + validate: () => Promise.resolve(true), +}; + +const ALWAYS_FALSE_RULES_VALIDATOR = { + validate: async () => Promise.resolve(false), +}; + +const ALWAYS_TRUE_ADMIN_CREDENTIAL_VALIDATOR = { + validate: () => true, +}; + +describe("files", () => { + // The storage emulator uses EmulatorRegistry to generate links in metadata. + before(async () => { + const emu = await FakeEmulator.create(Emulators.STORAGE); + await EmulatorRegistry.start(emu); + }); + after(async () => { + await EmulatorRegistry.stop(Emulators.STORAGE); + }); + + it("can serialize and deserialize metadata", () => { + const cf = new StorageCloudFunctions("demo-project"); + const metadata = new StoredFileMetadata( + { + name: "name", + bucket: "bucket", + contentType: "mime/type", + downloadTokens: ["token123"], + customMetadata: { + foo: "bar", + }, + }, + cf, + Buffer.from("Hello, World!"), + ); + + const json = StoredFileMetadata.toJSON(metadata); + const deserialized = StoredFileMetadata.fromJSON(json, cf); + expect(deserialized).to.deep.equal(metadata); + }); + + it("converts non-string custom metadata to string", () => { + const cf = new StorageCloudFunctions("demo-project"); + const customMetadata = { + foo: true as unknown as string, + }; + const metadata = new StoredFileMetadata( + { + customMetadata, + name: "name", + bucket: "bucket", + contentType: "mime/type", + downloadTokens: ["token123"], + }, + cf, + Buffer.from("Hello, World!"), + ); + const json = StoredFileMetadata.toJSON(metadata); + const deserialized = StoredFileMetadata.fromJSON(json, cf); + expect(deserialized.customMetadata).to.deep.equal({ foo: "true" }); + }); + + describe("StorageLayer", () => { + let _persistence: Persistence; + let _uploadService: UploadService; + + type UploadFileOptions = { + data?: string; + metadata?: Object; + }; + + async function uploadFile( + storageLayer: StorageLayer, + bucketId: string, + objectId: string, + opts?: UploadFileOptions, + ) { + const upload = _uploadService.multipartUpload({ + bucketId, + objectId: encodeURIComponent(objectId), + dataRaw: Buffer.from(opts?.data ?? "hello world"), + metadata: opts?.metadata ?? {}, + }); + await storageLayer.uploadObject(upload); + } + + beforeEach(() => { + _persistence = new Persistence(getPersistenceTmpDir()); + _uploadService = new UploadService(_persistence); + }); + + describe("#uploadObject()", () => { + it("should throw if upload is not finished", () => { + const storageLayer = getStorageLayer(ALWAYS_TRUE_RULES_VALIDATOR); + const upload = _uploadService.startResumableUpload({ + bucketId: "bucket", + objectId: "dir%2Fobject", + metadata: {}, + }); + + expect(storageLayer.uploadObject(upload)).to.be.rejectedWith("Unexpected upload status"); + }); + + it("should throw if upload is not authorized", () => { + const storageLayer = getStorageLayer(ALWAYS_FALSE_RULES_VALIDATOR); + const uploadId = _uploadService.startResumableUpload({ + bucketId: "bucket", + objectId: "dir%2Fobject", + metadata: {}, + }).id; + _uploadService.continueResumableUpload(uploadId, Buffer.from("hello world")); + const upload = _uploadService.finalizeResumableUpload(uploadId); + + expect(storageLayer.uploadObject(upload)).to.be.rejectedWith(ForbiddenError); + }); + }); + + describe("#getObject()", () => { + it("should return data and metadata", async () => { + const storageLayer = getStorageLayer(ALWAYS_TRUE_RULES_VALIDATOR); + await uploadFile(storageLayer, "bucket", "dir/object", { + data: "Hello, World!", + metadata: { contentType: "mime/type" }, + }); + + const { metadata, data } = await storageLayer.getObject({ + bucketId: "bucket", + decodedObjectId: "dir%2Fobject", + }); + + expect(metadata.contentType).to.equal("mime/type"); + expect(data.toString()).to.equal("Hello, World!"); + }); + + it("should throw an error if request is not authorized", () => { + const storageLayer = getStorageLayer(ALWAYS_FALSE_RULES_VALIDATOR); + + expect( + storageLayer.getObject({ + bucketId: "bucket", + decodedObjectId: "dir%2Fobject", + }), + ).to.be.rejectedWith(ForbiddenError); + }); + + it("should throw an error if the object does not exist", () => { + const storageLayer = getStorageLayer(ALWAYS_TRUE_RULES_VALIDATOR); + + expect( + storageLayer.getObject({ + bucketId: "bucket", + decodedObjectId: "dir%2Fobject", + }), + ).to.be.rejectedWith(NotFoundError); + }); + }); + + const getStorageLayer = (rulesValidator: FirebaseRulesValidator) => + new StorageLayer( + "project", + new Map(), + new Map(), + rulesValidator, + ALWAYS_TRUE_ADMIN_CREDENTIAL_VALIDATOR, + _persistence, + new StorageCloudFunctions("project"), + ); + + const getPersistenceTmpDir = () => `${tmpdir()}/firebase/storage/blobs`; + }); +}); diff --git a/src/emulator/storage/files.ts b/src/emulator/storage/files.ts index ecc9152ccad..d66084cccdb 100644 --- a/src/emulator/storage/files.ts +++ b/src/emulator/storage/files.ts @@ -1,19 +1,25 @@ -import { openSync, closeSync, readSync, unlinkSync, renameSync, existsSync, mkdirSync } from "fs"; -import { tmpdir } from "os"; -import { v4 } from "uuid"; -import { ListItem, ListResponse } from "./list"; +import { existsSync, readFileSync, readdirSync, statSync } from "fs"; import { CloudStorageBucketMetadata, CloudStorageObjectMetadata, IncomingMetadata, StoredFileMetadata, } from "./metadata"; +import { NotFoundError, ForbiddenError } from "./errors"; import * as path from "path"; -import * as fs from "fs"; import * as fse from "fs-extra"; -import * as rimraf from "rimraf"; import { StorageCloudFunctions } from "./cloudFunctions"; import { logger } from "../../logger"; +import { + constructDefaultAdminSdkConfig, + getProjectAdminSdkConfigOrCached, +} from "../adminSdkConfig"; +import { RulesetOperationMethod } from "./rules/types"; +import { AdminCredentialValidator, FirebaseRulesValidator } from "./rules/utils"; +import { Persistence } from "./persistence"; +import { Upload, UploadStatus } from "./upload"; +import { trackEmulator } from "../../track"; +import { Emulators } from "../types"; interface BucketsList { buckets: { @@ -29,110 +35,95 @@ export class StoredFile { public set metadata(value: StoredFileMetadata) { this._metadata = value; } - private _path: string; - - constructor(metadata: StoredFileMetadata, path: string) { + constructor(metadata: StoredFileMetadata) { this.metadata = metadata; - this._path = path; - } - public get path(): string { - return this._path; - } - public set path(value: string) { - this._path = value; } } -export class ResumableUpload { - private _uploadId: string; - private _metadata: IncomingMetadata; - private _bucketId: string; - private _objectId: string; - private _contentType: string; - private _currentBytesUploaded = 0; - private _status: UploadStatus = UploadStatus.ACTIVE; - private _fileLocation: string; +/** Parsed request object for {@link StorageLayer#getObject}. */ +export type GetObjectRequest = { + bucketId: string; + decodedObjectId: string; + authorization?: string; + downloadToken?: string; +}; - constructor( - bucketId: string, - objectId: string, - uploadId: string, - contentType: string, - metadata: IncomingMetadata - ) { - this._bucketId = bucketId; - this._objectId = objectId; - this._uploadId = uploadId; - this._contentType = contentType; - this._metadata = metadata; - this._fileLocation = encodeURIComponent(`${uploadId}_b_${bucketId}_o_${objectId}`); - this._currentBytesUploaded = 0; - } +/** Response object for {@link StorageLayer#getObject}. */ +export type GetObjectResponse = { + metadata: StoredFileMetadata; + data: Buffer; +}; - public get uploadId(): string { - return this._uploadId; - } - public get metadata(): IncomingMetadata { - return this._metadata; - } - public get bucketId(): string { - return this._bucketId; - } - public get objectId(): string { - return this._objectId; - } - public get contentType(): string { - return this._contentType; - } - public set contentType(contentType: string) { - this._contentType = contentType; - } - public get currentBytesUploaded(): number { - return this._currentBytesUploaded; - } - public set currentBytesUploaded(value: number) { - this._currentBytesUploaded = value; - } - public set status(status: UploadStatus) { - this._status = status; - } - public get status(): UploadStatus { - return this._status; - } - public get fileLocation(): string { - return this._fileLocation; - } -} +/** Parsed request object for {@link StorageLayer#updateObjectMetadata}. */ +export type UpdateObjectMetadataRequest = { + bucketId: string; + decodedObjectId: string; + metadata: IncomingMetadata; + authorization?: string; +}; -export enum UploadStatus { - ACTIVE, - CANCELLED, - FINISHED, -} +/** Parsed request object for {@link StorageLayer#deleteObject}. */ +export type DeleteObjectRequest = { + bucketId: string; + decodedObjectId: string; + authorization?: string; +}; -export type FinalizedUpload = { - upload: ResumableUpload; - file: StoredFile; +/** Parsed request object for {@link StorageLayer#listObjects}. */ +export type ListObjectsRequest = { + bucketId: string; + prefix: string; + delimiter: string; + pageToken?: string; + maxResults?: number; + authorization?: string; }; -export class StorageLayer { - private _files!: Map; - private _uploads!: Map; - private _buckets!: Map; - private _persistence!: Persistence; - private _cloudFunctions: StorageCloudFunctions; - - constructor(private _projectId: string) { - this.reset(); - this._cloudFunctions = new StorageCloudFunctions(this._projectId); - } +/** Response object for {@link StorageLayer#listObjects}. */ +export type ListObjectsResponse = { + prefixes?: string[]; + items?: StoredFileMetadata[]; + nextPageToken?: string; +}; - public reset(): void { - this._files = new Map(); - this._persistence = new Persistence(`${tmpdir()}/firebase/storage/blobs`); - this._uploads = new Map(); - this._buckets = new Map(); - } +/** Parsed request object for {@link StorageLayer#createDownloadToken}. */ +export type CreateDownloadTokenRequest = { + bucketId: string; + decodedObjectId: string; + authorization?: string; +}; + +/** Parsed request object for {@link StorageLayer#deleteDownloadToken}. */ +export type DeleteDownloadTokenRequest = { + bucketId: string; + decodedObjectId: string; + token: string; + authorization?: string; +}; + +/** Parsed request object for {@link StorageLayer#copyObject}. */ +export type CopyObjectRequest = { + sourceBucket: string; + sourceObject: string; + destinationBucket: string; + destinationObject: string; + incomingMetadata?: IncomingMetadata; + authorization?: string; +}; + +// Matches any number of "/" at the end of a string. +const TRAILING_SLASHES_PATTERN = /\/+$/; + +export class StorageLayer { + constructor( + private _projectId: string, + private _files: Map, + private _buckets: Map, + private _rulesValidator: FirebaseRulesValidator, + private _adminCredsValidator: AdminCredentialValidator, + private _persistence: Persistence, + private _cloudFunctions: StorageCloudFunctions, + ) {} createBucket(id: string): void { if (!this._buckets.has(id)) { @@ -140,15 +131,53 @@ export class StorageLayer { } } - listBuckets(): CloudStorageBucketMetadata[] { - if (this._buckets.size == 0) { - this.createBucket("default-bucket"); + async listBuckets(): Promise { + if (this._buckets.size === 0) { + let adminSdkConfig = await getProjectAdminSdkConfigOrCached(this._projectId); + if (!adminSdkConfig) { + adminSdkConfig = constructDefaultAdminSdkConfig(this._projectId); + } + this.createBucket(adminSdkConfig.storageBucket!); } return [...this._buckets.values()]; } - public getMetadata(bucket: string, object: string): StoredFileMetadata | undefined { + /** + * Returns an stored object and its metadata. + * @throws {NotFoundError} if object does not exist + * @throws {ForbiddenError} if request is unauthorized + */ + public async getObject(request: GetObjectRequest): Promise { + const metadata = this.getMetadata(request.bucketId, request.decodedObjectId); + + // If a valid download token is present, skip Firebase Rules auth. Mainly used by the js sdk. + const hasValidDownloadToken = (metadata?.downloadTokens || []).includes( + request.downloadToken ?? "", + ); + let authorized = hasValidDownloadToken; + if (!authorized) { + authorized = await this._rulesValidator.validate( + ["b", request.bucketId, "o", request.decodedObjectId].join("/"), + request.bucketId, + RulesetOperationMethod.GET, + { before: metadata?.asRulesResource() }, + this._projectId, + request.authorization, + ); + } + if (!authorized) { + throw new ForbiddenError("Failed auth"); + } + + if (!metadata) { + throw new NotFoundError("File not found"); + } + + return { metadata: metadata!, data: this.getBytes(request.bucketId, request.decodedObjectId)! }; + } + + private getMetadata(bucket: string, object: string): StoredFileMetadata | undefined { const key = this.path(bucket, object); const val = this._files.get(key); @@ -159,11 +188,11 @@ export class StorageLayer { return; } - public getBytes( + private getBytes( bucket: string, object: string, size?: number, - offset?: number + offset?: number, ): Buffer | undefined { const key = this.path(bucket, object); const val = this._files.get(key); @@ -173,48 +202,31 @@ export class StorageLayer { } return undefined; } - - public(value: Map) { - this._files = value; - } - - public startUpload( - bucket: string, - object: string, - contentType: string, - metadata: IncomingMetadata - ): ResumableUpload { - const uploadId = v4(); - const upload = new ResumableUpload(bucket, object, uploadId, contentType, metadata); - this._uploads.set(uploadId, upload); - return upload; - } - - public queryUpload(uploadId: string): ResumableUpload | undefined { - return this._uploads.get(uploadId); - } - - public cancelUpload(uploadId: string): ResumableUpload | undefined { - const upload = this._uploads.get(uploadId); - if (!upload) { - return undefined; + /** + * Deletes an object. + * @throws {ForbiddenError} if the request is not authorized. + * @throws {NotFoundError} if the object does not exist. + */ + public async deleteObject(request: DeleteObjectRequest): Promise { + const storedMetadata = this.getMetadata(request.bucketId, request.decodedObjectId); + const authorized = await this._rulesValidator.validate( + ["b", request.bucketId, "o", request.decodedObjectId].join("/"), + request.bucketId, + RulesetOperationMethod.DELETE, + { before: storedMetadata?.asRulesResource() }, + this._projectId, + request.authorization, + ); + if (!authorized) { + throw new ForbiddenError(); } - upload.status = UploadStatus.CANCELLED; - this._persistence.deleteFile(upload.fileLocation); - } - - public uploadBytes(uploadId: string, bytes: Buffer): ResumableUpload | undefined { - const upload = this._uploads.get(uploadId); - - if (!upload) { - return undefined; + if (!storedMetadata) { + throw new NotFoundError(); } - this._persistence.appendBytes(upload.fileLocation, bytes, upload.currentBytesUploaded); - upload.currentBytesUploaded += bytes.byteLength; - return upload; + this.deleteFile(request.bucketId, request.decodedObjectId); } - public deleteFile(bucketId: string, objectId: string): boolean { + private deleteFile(bucketId: string, objectId: string): boolean { const isFolder = objectId.toLowerCase().endsWith("%2f"); if (isFolder) { @@ -229,7 +241,7 @@ export class StorageLayer { const file = this._files.get(filePath); - if (file == undefined) { + if (file === undefined) { return false; } else { this._files.delete(filePath); @@ -240,244 +252,278 @@ export class StorageLayer { } } - public async deleteAll(): Promise { - return this._persistence.deleteAll(); - } + /** + * Updates an existing object's metadata. + * @throws {ForbiddenError} if the request is not authorized. + * @throws {NotFoundError} if the object does not exist. + */ + public async updateObjectMetadata( + request: UpdateObjectMetadataRequest, + ): Promise { + const storedMetadata = this.getMetadata(request.bucketId, request.decodedObjectId); + const authorized = await this._rulesValidator.validate( + ["b", request.bucketId, "o", request.decodedObjectId].join("/"), + request.bucketId, + RulesetOperationMethod.UPDATE, + { + before: storedMetadata?.asRulesResource(), + after: storedMetadata?.asRulesResource(request.metadata), + }, + this._projectId, + request.authorization, + ); + if (!authorized) { + throw new ForbiddenError(); + } + if (!storedMetadata) { + throw new NotFoundError(); + } - public finalizeUpload(uploadId: string): FinalizedUpload | undefined { - const upload = this._uploads.get(uploadId); + storedMetadata.update(request.metadata); + return storedMetadata; + } - if (!upload) { - return undefined; + /** + * Last step in uploading a file. Validates the request and persists the staging + * object to its permanent location on disk, updates metadata. + */ + public async uploadObject(upload: Upload): Promise { + if (upload.status !== UploadStatus.FINISHED) { + throw new Error(`Unexpected upload status encountered: ${upload.status}.`); } - upload.status = UploadStatus.FINISHED; + const storedMetadata = this.getMetadata(upload.bucketId, upload.objectId); const filePath = this.path(upload.bucketId, upload.objectId); - - const bytes = this._persistence.readBytes(upload.fileLocation, upload.currentBytesUploaded); - const finalMetadata = new StoredFileMetadata( + // Pulls fields out of upload.metadata and ignores null values. + function getIncomingMetadata(field: string): any { + if (!upload.metadata) { + return undefined; + } + const value: any | undefined = (upload.metadata! as any)[field]; + return value === null ? undefined : value; + } + const metadata = new StoredFileMetadata( { name: upload.objectId, bucket: upload.bucketId, - contentType: "", - contentEncoding: upload.metadata.contentEncoding, - customMetadata: upload.metadata.metadata, + contentType: getIncomingMetadata("contentType"), + contentDisposition: getIncomingMetadata("contentDisposition"), + contentEncoding: getIncomingMetadata("contentEncoding"), + contentLanguage: getIncomingMetadata("contentLanguage"), + cacheControl: getIncomingMetadata("cacheControl"), + customMetadata: getIncomingMetadata("metadata"), }, this._cloudFunctions, - bytes, - upload.metadata + this._persistence.readBytes(upload.path, upload.size), ); - const file = new StoredFile(finalMetadata, filePath); - this._files.set(filePath, file); - - this._persistence.deleteFile(filePath, true); - this._persistence.renameFile(upload.fileLocation, filePath); - - this._cloudFunctions.dispatch("finalize", new CloudStorageObjectMetadata(file.metadata)); - return { upload: upload, file: file }; - } - - public oneShotUpload( - bucket: string, - object: string, - contentType: string, - incomingMetadata: IncomingMetadata, - bytes: Buffer - ) { - const filePath = this.path(bucket, object); - this._persistence.deleteFile(filePath, true); + const authorized = await this._rulesValidator.validate( + ["b", upload.bucketId, "o", upload.objectId].join("/"), + upload.bucketId, + RulesetOperationMethod.CREATE, + { + before: storedMetadata?.asRulesResource(), + after: metadata.asRulesResource(), + }, + this._projectId, + upload.authorization, + ); + if (!authorized) { + this._persistence.deleteFile(upload.path); + throw new ForbiddenError(); + } + + // Persist to permanent location on disk. + this._persistence.deleteFile(filePath, /* failSilently = */ true); + this._persistence.renameFile(upload.path, filePath); + this._files.set(filePath, new StoredFile(metadata)); + this._cloudFunctions.dispatch("finalize", new CloudStorageObjectMetadata(metadata)); + return metadata; + } + + public copyObject({ + sourceBucket, + sourceObject, + destinationBucket, + destinationObject, + incomingMetadata, + authorization, + }: CopyObjectRequest): StoredFileMetadata { + if (!this._adminCredsValidator.validate(authorization)) { + throw new ForbiddenError(); + } + const sourceMetadata = this.getMetadata(sourceBucket, sourceObject); + if (!sourceMetadata) { + throw new NotFoundError(); + } + const sourceBytes = this.getBytes(sourceBucket, sourceObject) as Buffer; + + const destinationFilePath = this.path(destinationBucket, destinationObject); + this._persistence.deleteFile(destinationFilePath, /* failSilently = */ true); + this._persistence.appendBytes(destinationFilePath, sourceBytes); + + const newMetadata: IncomingMetadata = { + ...sourceMetadata, + metadata: sourceMetadata.customMetadata, + ...incomingMetadata, + }; + if ( + sourceMetadata.downloadTokens.length && + // Only copy download tokens if we're not overwriting any custom metadata + !(incomingMetadata?.metadata && Object.keys(incomingMetadata?.metadata).length) + ) { + if (!newMetadata.metadata) newMetadata.metadata = {}; + newMetadata.metadata.firebaseStorageDownloadTokens = sourceMetadata.downloadTokens.join(","); + } + if (newMetadata.metadata) { + // Convert null metadata values to empty strings + for (const [k, v] of Object.entries(newMetadata.metadata)) { + if (v === null) newMetadata.metadata[k] = ""; + } + } - this._persistence.appendBytes(filePath, bytes); - const md = new StoredFileMetadata( + // Pulls fields out of newMetadata and ignores null values. + function getMetadata(field: string): any { + const value: any | undefined = (newMetadata as any)[field]; + return value === null ? undefined : value; + } + const copiedFileMetadata = new StoredFileMetadata( { - name: object, - bucket: bucket, - contentType: incomingMetadata.contentType || "application/octet-stream", - contentEncoding: incomingMetadata.contentEncoding, - customMetadata: incomingMetadata.metadata, + name: destinationObject, + bucket: destinationBucket, + contentType: getMetadata("contentType"), + contentDisposition: getMetadata("contentDisposition"), + contentEncoding: getMetadata("contentEncoding"), + contentLanguage: getMetadata("contentLanguage"), + cacheControl: getMetadata("cacheControl"), + customMetadata: getMetadata("metadata"), }, this._cloudFunctions, - bytes, - incomingMetadata + sourceBytes, ); - const file = new StoredFile(md, this._persistence.getDiskPath(filePath)); - this._files.set(filePath, file); + const file = new StoredFile(copiedFileMetadata); + this._files.set(destinationFilePath, file); this._cloudFunctions.dispatch("finalize", new CloudStorageObjectMetadata(file.metadata)); return file.metadata; } - public listItemsAndPrefixes( - bucket: string, - prefix: string, - delimiter: string, - pageToken: string | undefined, - maxResults: number | undefined - ): ListResponse { - if (!delimiter) { - delimiter = "/"; - } - - if (!prefix) { - prefix = ""; - } - - if (!prefix.endsWith(delimiter)) { - prefix += delimiter; - } - - if (!prefix.startsWith(delimiter)) { - prefix = delimiter + prefix; + /** + * Lists all files and prefixes (folders) at a path. + * @throws {ForbiddenError} if the request is not authorized. + */ + public async listObjects(request: ListObjectsRequest): Promise { + const { bucketId, prefix, delimiter, pageToken, authorization } = request; + + const authorized = await this._rulesValidator.validate( + // Firebase Rules expects the path without trailing slashes. + ["b", bucketId, "o", prefix.replace(TRAILING_SLASHES_PATTERN, "")].join("/"), + bucketId, + RulesetOperationMethod.LIST, + {}, + this._projectId, + authorization, + delimiter, + ); + if (!authorized) { + throw new ForbiddenError(); } - let items = []; + let items: Array = []; const prefixes = new Set(); for (const [, file] of this._files) { - if (file.metadata.bucket != bucket) { + if (file.metadata.bucket !== bucketId) { continue; } - let name = `${delimiter}${file.metadata.name}`; + const name = file.metadata.name; if (!name.startsWith(prefix)) { continue; } - name = name.substring(prefix.length); - if (name.startsWith(delimiter)) { - name = name.substring(prefix.length); + let includeMetadata = true; + if (delimiter) { + const delimiterIdx = name.indexOf(delimiter); + const delimiterAfterPrefixIdx = name.indexOf(delimiter, prefix.length); + // items[] contains object metadata for objects whose names do not contain + // delimiter, or whose names only have instances of delimiter in their prefix. + includeMetadata = delimiterIdx === -1 || delimiterAfterPrefixIdx === -1; + if (delimiterAfterPrefixIdx !== -1) { + // prefixes[] contains truncated object names for objects whose names contain + // delimiter after any prefix. Object names are truncated beyond the first + // applicable instance of the delimiter. + prefixes.add(name.slice(0, delimiterAfterPrefixIdx + delimiter.length)); + } } - const startAtIndex = name.indexOf(delimiter); - if (startAtIndex == -1) { - if (!file.metadata.name.endsWith("/")) { - items.push(file.metadata.name); - } - } else { - const prefixPath = prefix + name.substring(0, startAtIndex + 1); - prefixes.add(prefixPath); + if (includeMetadata) { + items.push(file.metadata); } } - items.sort(); + // Order items by name + items.sort((a, b) => { + if (a.name === b.name) { + return 0; + } else if (a.name < b.name) { + return -1; + } else { + return 1; + } + }); if (pageToken) { - const idx = items.findIndex((v) => v == pageToken); - if (idx != -1) { + const idx = items.findIndex((v) => v.name === pageToken); + if (idx !== -1) { items = items.slice(idx); } } - if (!maxResults) { - maxResults = 1000; - } - + const maxResults = request.maxResults ?? 1000; let nextPageToken = undefined; if (items.length > maxResults) { - nextPageToken = items[maxResults]; + nextPageToken = items[maxResults].name; items = items.slice(0, maxResults); } - return new ListResponse( - [...prefixes].sort(), - items.map((i) => new ListItem(i, bucket)), - nextPageToken - ); - } - - public listItems( - bucket: string, - prefix: string, - delimiter: string, - pageToken: string | undefined, - maxResults: number | undefined - ) { - if (!delimiter) { - delimiter = "/"; - } - - if (!prefix) { - prefix = ""; - } - - if (!prefix.endsWith(delimiter)) { - prefix += delimiter; - } - - let items = []; - for (const [, file] of this._files) { - if (file.metadata.bucket != bucket) { - continue; - } - - let name = file.metadata.name; - if (!name.startsWith(prefix)) { - continue; - } - - name = name.substring(prefix.length); - if (name.startsWith(delimiter)) { - name = name.substring(prefix.length); - } - - items.push(this.path(file.metadata.bucket, file.metadata.name)); - } - - items.sort(); - if (pageToken) { - const idx = items.findIndex((v) => v == pageToken); - if (idx != -1) { - items = items.slice(idx); - } - } - - if (!maxResults) { - maxResults = 1000; - } - return { - kind: "#storage/objects", - items: items.map((item) => { - const storedFile = this._files.get(item); - if (!storedFile) { - return console.warn(`No file ${item}`); - } - - return new CloudStorageObjectMetadata(storedFile.metadata); - }), + nextPageToken, + prefixes: prefixes.size > 0 ? [...prefixes].sort() : undefined, + items: items.length > 0 ? items : undefined, }; } - public addDownloadToken(bucket: string, object: string): StoredFileMetadata | undefined { - const key = this.path(bucket, object); - const val = this._files.get(key); - if (!val) { - return undefined; + /** Creates a new Firebase download token for an object. */ + public createDownloadToken(request: CreateDownloadTokenRequest): StoredFileMetadata { + if (!this._adminCredsValidator.validate(request.authorization)) { + throw new ForbiddenError(); + } + const metadata = this.getMetadata(request.bucketId, request.decodedObjectId); + if (!metadata) { + throw new NotFoundError(); } - const md = val.metadata; - md.addDownloadToken(); - return md; + metadata.addDownloadToken(); + return metadata; } - public deleteDownloadToken( - bucket: string, - object: string, - token: string - ): StoredFileMetadata | undefined { - const key = this.path(bucket, object); - const val = this._files.get(key); - if (!val) { - return undefined; + /** + * Removes a Firebase download token from an object's metadata. If the token is not already + * present, calling this method is a no-op. This method will also regenerate a new token + * if the last remaining token is deleted. + */ + public deleteDownloadToken(request: DeleteDownloadTokenRequest): StoredFileMetadata { + if (!this._adminCredsValidator.validate(request.authorization)) { + throw new ForbiddenError(); } - const md = val.metadata; - md.deleteDownloadToken(token); - return md; + const metadata = this.getMetadata(request.bucketId, request.decodedObjectId); + if (!metadata) { + throw new NotFoundError(); + } + metadata.deleteDownloadToken(request.token); + return metadata; } private path(bucket: string, object: string): string { - const directory = path.dirname(object); - const filename = path.basename(object) + (object.endsWith("/") ? "/" : ""); - - return path.join(bucket, directory, encodeURIComponent(filename)); + return path.join(bucket, object); } public get dirPath(): string { @@ -488,32 +534,43 @@ export class StorageLayer { * Export is implemented using async operations so that it does not block * the hub when invoked. */ - async export(storageExportPath: string) { + async export(storageExportPath: string, options: { initiatedBy: string }): Promise { // Export a list of all known bucket IDs, which can be used to reconstruct // the bucket metadata. const bucketsList: BucketsList = { buckets: [], }; - for (const b of this.listBuckets()) { + for (const b of await this.listBuckets()) { bucketsList.buckets.push({ id: b.id }); } + void trackEmulator("emulator_export", { + initiated_by: options.initiatedBy, + emulator_name: Emulators.STORAGE, + count: bucketsList.buckets.length, + }); + // Resulting path is platform-specific, e.g. foo%5Cbar on Windows, foo%2Fbar on Linux + // after URI encoding. Similarly for metadata paths below. const bucketsFilePath = path.join(storageExportPath, "buckets.json"); await fse.writeFile(bucketsFilePath, JSON.stringify(bucketsList, undefined, 2)); - // Recursively copy all file blobs + // Create blobs directory const blobsDirPath = path.join(storageExportPath, "blobs"); await fse.ensureDir(blobsDirPath); - await fse.copy(this.dirPath, blobsDirPath, { recursive: true }); - // Store a metadata file for each file + // Create metadata directory const metadataDirPath = path.join(storageExportPath, "metadata"); await fse.ensureDir(metadataDirPath); - for await (const [p, file] of this._files.entries()) { - const metadataExportPath = path.join(metadataDirPath, p) + ".json"; - const metadataExportDirPath = path.dirname(metadataExportPath); + // Copy data into metadata and blobs directory + for await (const [, file] of this._files.entries()) { + // get diskFilename from file path, metadata and blob files are persisted with this name + const diskFileName = this._persistence.getDiskFileName( + this.path(file.metadata.bucket, file.metadata.name), + ); - await fse.ensureDir(metadataExportDirPath); + await fse.copy(path.join(this.dirPath, diskFileName), path.join(blobsDirPath, diskFileName)); + const metadataExportPath = + path.join(metadataDirPath, encodeURIComponent(diskFileName)) + ".json"; await fse.writeFile(metadataExportPath, StoredFileMetadata.toJSON(file.metadata)); } } @@ -522,10 +579,16 @@ export class StorageLayer { * Import can be implemented using sync operations because the emulator should * not be handling any other requests during import. */ - import(storageExportPath: string) { + import(storageExportPath: string, options: { initiatedBy: string }): void { // Restore list of buckets const bucketsFile = path.join(storageExportPath, "buckets.json"); - const bucketsList = JSON.parse(fs.readFileSync(bucketsFile, "utf-8")) as BucketsList; + const bucketsList = JSON.parse(readFileSync(bucketsFile, "utf-8")) as BucketsList; + void trackEmulator("emulator_import", { + initiated_by: options.initiatedBy, + emulator_name: Emulators.STORAGE, + count: bucketsList.buckets.length, + }); + for (const b of bucketsList.buckets) { const bucketMetadata = new CloudStorageBucketMetadata(b.id); this._buckets.set(b.id, bucketMetadata); @@ -534,6 +597,14 @@ export class StorageLayer { const metadataDir = path.join(storageExportPath, "metadata"); const blobsDir = path.join(storageExportPath, "blobs"); + // Handle case where export contained empty metadata or blobs + if (!existsSync(metadataDir) || !existsSync(blobsDir)) { + logger.warn( + `Could not find metadata directory at "${metadataDir}" and/or blobs directory at "${blobsDir}".`, + ); + return; + } + // Restore all metadata const metadataList = this.walkDirSync(metadataDir); @@ -543,10 +614,7 @@ export class StorageLayer { logger.debug(`Skipping unexpected storage metadata file: ${f}`); continue; } - const metadata = StoredFileMetadata.fromJSON( - fs.readFileSync(f, "utf-8"), - this._cloudFunctions - ); + const metadata = StoredFileMetadata.fromJSON(readFileSync(f, "utf-8"), this._cloudFunctions); // To get the blob path from the metadata path: // 1) Get the relative path to the metadata export dir @@ -555,24 +623,30 @@ export class StorageLayer { const blobPath = metadataRelPath.substring(0, metadataRelPath.length - dotJson.length); const blobAbsPath = path.join(blobsDir, blobPath); - if (!fs.existsSync(blobAbsPath)) { + if (!existsSync(blobAbsPath)) { logger.warn(`Could not find file "${blobPath}" in storage export.`); continue; } - const file = new StoredFile(metadata, blobPath); - this._files.set(blobPath, file); - } + let fileName = metadata.name; + const objectNameSep = getPathSep(fileName); + // Replace all file separators with that of current platform for compatibility + if (fileName !== path.sep) { + fileName = fileName.split(objectNameSep).join(path.sep); + } + + const filepath = this.path(metadata.bucket, fileName); - // Recursively copy all blobs - fse.copySync(blobsDir, this.dirPath); + this._persistence.copyFromExternalPath(blobAbsPath, filepath); + this._files.set(filepath, new StoredFile(metadata)); + } } private *walkDirSync(dir: string): Generator { - const files = fs.readdirSync(dir); + const files = readdirSync(dir); for (const file of files) { const p = path.join(dir, file); - if (fs.statSync(p).isDirectory()) { + if (statSync(p).isDirectory()) { yield* this.walkDirSync(p); } else { yield p; @@ -581,100 +655,9 @@ export class StorageLayer { } } -export class Persistence { - private _dirPath: string; - constructor(dirPath: string) { - this._dirPath = dirPath; - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { - recursive: true, - }); - } - } - - public get dirPath(): string { - return this._dirPath; - } - - appendBytes(fileName: string, bytes: Buffer, fileOffset?: number): string { - const filepath = this.getDiskPath(fileName); - - const encodedSlashIndex = filepath.toLowerCase().lastIndexOf("%2f"); - const dirPath = - encodedSlashIndex >= 0 ? filepath.substring(0, encodedSlashIndex) : path.dirname(filepath); - - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { - recursive: true, - }); - } - let fd; - - try { - // TODO: This is more technically correct, but corrupts multipart files - // fd = openSync(path, "w+"); - // writeSync(fd, bytes, 0, bytes.byteLength, fileOffset); - - fs.appendFileSync(filepath, bytes); - return filepath; - } finally { - if (fd) { - closeSync(fd); - } - } - } - - readBytes(fileName: string, size: number, fileOffset?: number): Buffer { - const path = this.getDiskPath(fileName); - let fd; - try { - fd = openSync(path, "r"); - const buf = Buffer.alloc(size); - const offset = fileOffset && fileOffset > 0 ? fileOffset : 0; - readSync(fd, buf, 0, size, offset); - return buf; - } finally { - if (fd) { - closeSync(fd); - } - } - } - - deleteFile(fileName: string, failSilently = false): void { - try { - unlinkSync(this.getDiskPath(fileName)); - } catch (err) { - if (!failSilently) { - throw err; - } - } - } - - deleteAll(): Promise { - return new Promise((resolve, reject) => { - rimraf(this._dirPath, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - renameFile(oldName: string, newName: string): void { - const dirPath = this.getDiskPath(path.dirname(newName)); - - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { - recursive: true, - }); - } - - renameSync(this.getDiskPath(oldName), this.getDiskPath(newName)); - } - - getDiskPath(fileName: string): string { - return path.join(this._dirPath, fileName); - } +/** Returns file separator used in given path, either '\\' or '/'. */ +function getPathSep(decodedPath: string): string { + // Checks for the first matching file separator + const firstSepIndex = decodedPath.search(/[\/|\\\\]/g); + return decodedPath[firstSepIndex]; } diff --git a/src/emulator/storage/index.ts b/src/emulator/storage/index.ts index a7d3b0565d9..08c648d8c87 100644 --- a/src/emulator/storage/index.ts +++ b/src/emulator/storage/index.ts @@ -1,164 +1,130 @@ -import * as path from "path"; +import { tmpdir } from "os"; import * as utils from "../../utils"; import { Constants } from "../constants"; import { EmulatorInfo, EmulatorInstance, Emulators } from "../types"; import { createApp } from "./server"; -import { StorageLayer } from "./files"; -import * as chokidar from "chokidar"; +import { StorageLayer, StoredFile } from "./files"; import { EmulatorLogger } from "../emulatorLogger"; -import * as fs from "fs"; -import { StorageRulesetInstance, StorageRulesRuntime, StorageRulesIssues } from "./rules/runtime"; -import { Source } from "./rules/types"; -import { FirebaseError } from "../../error"; -import { getDownloadDetails } from "../downloadableEmulators"; -import express = require("express"); +import { createStorageRulesManager, StorageRulesManager } from "./rules/manager"; +import { StorageRulesIssues, StorageRulesRuntime } from "./rules/runtime"; +import { SourceFile } from "./rules/types"; +import * as express from "express"; +import { + getAdminCredentialValidator, + getAdminOnlyFirebaseRulesValidator, + getFirebaseRulesValidator, + FirebaseRulesValidator, +} from "./rules/utils"; +import { Persistence } from "./persistence"; +import { UploadService } from "./upload"; +import { CloudStorageBucketMetadata } from "./metadata"; +import { StorageCloudFunctions } from "./cloudFunctions"; + +export type RulesConfig = { + resource: string; + rules: SourceFile; +}; export interface StorageEmulatorArgs { projectId: string; port?: number; host?: string; - rules: Source | string; + + // Either a single set of rules to be applied to all resources or a mapping of resource to rules + rules: SourceFile | RulesConfig[]; + auto_download?: boolean; } export class StorageEmulator implements EmulatorInstance { private destroyServer?: () => Promise; private _app?: express.Express; - private _rulesWatcher?: chokidar.FSWatcher; - private _rules?: StorageRulesetInstance; - private _rulesetSource?: Source; private _logger = EmulatorLogger.forEmulator(Emulators.STORAGE); private _rulesRuntime: StorageRulesRuntime; + private _rulesManager!: StorageRulesManager; + private _files: Map = new Map(); + private _buckets: Map = new Map(); + private _cloudFunctions: StorageCloudFunctions; + private _persistence: Persistence; + private _uploadService: UploadService; private _storageLayer: StorageLayer; + /** StorageLayer that validates requests solely based on admin credentials. */ + private _adminStorageLayer: StorageLayer; constructor(private args: StorageEmulatorArgs) { - const downloadDetails = getDownloadDetails(Emulators.STORAGE); this._rulesRuntime = new StorageRulesRuntime(); - this._storageLayer = new StorageLayer(args.projectId); + this._rulesManager = this.createRulesManager(this.args.rules); + this._cloudFunctions = new StorageCloudFunctions(args.projectId); + this._persistence = new Persistence(this.getPersistenceTmpDir()); + this._uploadService = new UploadService(this._persistence); + + const createStorageLayer = (rulesValidator: FirebaseRulesValidator): StorageLayer => { + return new StorageLayer( + args.projectId, + this._files, + this._buckets, + rulesValidator, + getAdminCredentialValidator(), + this._persistence, + this._cloudFunctions, + ); + }; + this._storageLayer = createStorageLayer( + getFirebaseRulesValidator((resource: string) => this._rulesManager.getRuleset(resource)), + ); + this._adminStorageLayer = createStorageLayer(getAdminOnlyFirebaseRulesValidator()); } get storageLayer(): StorageLayer { return this._storageLayer; } - get rules(): StorageRulesetInstance | undefined { - return this._rules; + get adminStorageLayer(): StorageLayer { + return this._adminStorageLayer; + } + + get uploadService(): UploadService { + return this._uploadService; + } + + get rulesManager(): StorageRulesManager { + return this._rulesManager; } get logger(): EmulatorLogger { return this._logger; } + reset(): void { + this._files.clear(); + this._buckets.clear(); + this._persistence.reset(this.getPersistenceTmpDir()); + this._uploadService.reset(); + } + async start(): Promise { const { host, port } = this.getInfo(); await this._rulesRuntime.start(this.args.auto_download); + await this._rulesManager.start(); this._app = await createApp(this.args.projectId, this); - - if (typeof this.args.rules == "string") { - const rulesFile = this.args.rules; - this.updateRulesSource(rulesFile); - } else { - this._rulesetSource = this.args.rules; - } - - if (!this._rulesetSource || this._rulesetSource.files.length == 0) { - throw new FirebaseError("Can not initialize Storage emulator without a rules source / file."); - } else if (this._rulesetSource.files.length > 1) { - throw new FirebaseError( - "Can not initialize Storage emulator with more than one rules source / file." - ); - } - - await this.loadRuleset(); - - const rulesPath = this._rulesetSource.files[0].name; - this._rulesWatcher = chokidar.watch(rulesPath, { persistent: true, ignoreInitial: true }); - this._rulesWatcher.on("change", async () => { - // There have been some race conditions reported (on Windows) where reading the - // file too quickly after the watcher fires results in an empty file being read. - // Adding a small delay prevents that at very little cost. - await new Promise((res) => setTimeout(res, 5)); - - this._logger.logLabeled( - "BULLET", - "storage", - `Change detected, updating rules for Cloud Storage...` - ); - this.updateRulesSource(rulesPath); - await this.loadRuleset(); - }); - const server = this._app.listen(port, host); this.destroyServer = utils.createDestroyer(server); } - private updateRulesSource(rulesFile: string): void { - this._rulesetSource = { - files: [ - { - name: rulesFile, - content: fs.readFileSync(rulesFile).toString(), - }, - ], - }; - } - - public async loadRuleset(source?: Source): Promise { - if (source) { - this._rulesetSource = source; - } - - if (!this._rulesetSource) { - const msg = "Attempting to update ruleset without a source."; - this._logger.log("WARN", msg); - - const error = JSON.stringify({ error: msg }); - return new StorageRulesIssues([error], []); - } - - const { ruleset, issues } = await this._rulesRuntime.loadRuleset(this._rulesetSource); - - if (!ruleset) { - issues.all.forEach((issue) => { - let parsedIssue; - try { - parsedIssue = JSON.parse(issue); - } catch { - // Parse manually - } - - if (parsedIssue) { - this._logger.log( - "WARN", - `${parsedIssue.description_.replace(/\.$/, "")} in ${ - parsedIssue.sourcePosition_.fileName_ - }:${parsedIssue.sourcePosition_.line_}` - ); - } else { - this._logger.log("WARN", issue); - } - }); - - delete this._rules; - } else { - this._rules = ruleset; - } - - return issues; - } - async connect(): Promise { // No-op } async stop(): Promise { - await this.storageLayer.deleteAll(); + await this._persistence.deleteAll(); + await this._rulesRuntime.stop(); + await this._rulesManager.stop(); return this.destroyServer ? this.destroyServer() : Promise.resolve(); } getInfo(): EmulatorInfo { - const host = this.args.host || Constants.getDefaultHost(Emulators.STORAGE); + const host = this.args.host || Constants.getDefaultHost(); const port = this.args.port || Constants.getDefaultPort(Emulators.STORAGE); return { @@ -175,4 +141,18 @@ export class StorageEmulator implements EmulatorInstance { getApp(): express.Express { return this._app!; } + + private createRulesManager(rules: SourceFile | RulesConfig[]): StorageRulesManager { + return createStorageRulesManager(rules, this._rulesRuntime); + } + + async replaceRules(rules: SourceFile | RulesConfig[]): Promise { + await this._rulesManager.stop(); + this._rulesManager = this.createRulesManager(rules); + return this._rulesManager.start(); + } + + private getPersistenceTmpDir(): string { + return `${tmpdir()}/firebase/storage/blobs`; + } } diff --git a/src/emulator/storage/list.ts b/src/emulator/storage/list.ts deleted file mode 100644 index ca86ee00ad2..00000000000 --- a/src/emulator/storage/list.ts +++ /dev/null @@ -1,20 +0,0 @@ -export class ListItem { - name: string; - bucket: string; - constructor(name: string, bucket: string) { - this.name = name; - this.bucket = bucket; - } -} - -export class ListResponse { - prefixes: string[]; - items: ListItem[]; - nextPageToken: string | undefined; - - constructor(prefixes: string[], items: ListItem[], nextPageToken: string | undefined) { - this.prefixes = prefixes; - this.items = items; - this.nextPageToken = nextPageToken; - } -} diff --git a/src/emulator/storage/metadata.spec.ts b/src/emulator/storage/metadata.spec.ts new file mode 100644 index 00000000000..6df67ca05a6 --- /dev/null +++ b/src/emulator/storage/metadata.spec.ts @@ -0,0 +1,15 @@ +import { expect } from "chai"; +import { toSerializedDate } from "./metadata"; + +describe("toSerializedDate", () => { + it("correctly serializes date", () => { + const testDate = new Date("2022-01-01T00:00:00.000Z"); + + expect(toSerializedDate(testDate)).to.equal("2022-01-01T00:00:00.000Z"); + }); + it("correctly serializes date with different timezone", () => { + const testDate = new Date("2022-01-01T00:00:00.000+07:00"); + + expect(toSerializedDate(testDate)).to.equal("2021-12-31T17:00:00.000Z"); + }); +}); diff --git a/src/emulator/storage/metadata.ts b/src/emulator/storage/metadata.ts index 50a7015f15e..1116fc9bf38 100644 --- a/src/emulator/storage/metadata.ts +++ b/src/emulator/storage/metadata.ts @@ -3,11 +3,7 @@ import * as crypto from "crypto"; import { EmulatorRegistry } from "../registry"; import { Emulators } from "../types"; import { StorageCloudFunctions } from "./cloudFunctions"; -import { crc32c } from "./crc"; - -type RulesResourceMetadataOverrides = { - [Property in keyof RulesResourceMetadata]?: RulesResourceMetadata[Property]; -}; +import { crc32c, crc32cToString } from "./crc"; type SerializedFileMetadata = Omit & { timeCreated: string; @@ -23,14 +19,14 @@ export class StoredFileMetadata { bucket: string; generation: number; metageneration: number; - contentType: string; + contentType?: string; timeCreated: Date; updated: Date; storageClass: string; size: number; md5Hash: string; - contentEncoding: string; - contentDisposition: string; + contentEncoding?: string; + contentDisposition?: string; contentLanguage?: string; cacheControl?: string; customTime?: Date; @@ -43,29 +39,40 @@ export class StoredFileMetadata { opts: Partial & { name: string; bucket: string; - contentType: string; }, private _cloudFunctions: StorageCloudFunctions, bytes?: Buffer, - incomingMetadata?: IncomingMetadata ) { // Required fields this.name = opts.name; this.bucket = opts.bucket; - this.contentType = opts.contentType; // Optional fields this.metageneration = opts.metageneration || 1; this.generation = opts.generation || Date.now(); + this.contentType = opts.contentType || "application/octet-stream"; this.storageClass = opts.storageClass || "STANDARD"; - this.etag = opts.etag || "someETag"; - this.contentDisposition = opts.contentDisposition || "inline"; + this.contentDisposition = opts.contentDisposition; this.cacheControl = opts.cacheControl; this.contentLanguage = opts.contentLanguage; this.customTime = opts.customTime; - this.contentEncoding = opts.contentEncoding || "identity"; - this.customMetadata = opts.customMetadata; + this.contentEncoding = opts.contentEncoding; this.downloadTokens = opts.downloadTokens || []; + if (opts.etag) { + this.etag = opts.etag; + } else { + this.etag = generateETag(this.generation, this.metageneration); + } + if (opts.customMetadata) { + this.customMetadata = {}; + for (const [k, v] of Object.entries(opts.customMetadata)) { + let stringVal = v; + if (typeof stringVal !== "string") { + stringVal = JSON.stringify(v); + } + this.customMetadata[k] = stringVal || ""; + } + } // Special handling for date fields this.timeCreated = opts.timeCreated ? new Date(opts.timeCreated) : new Date(); @@ -84,51 +91,60 @@ export class StoredFileMetadata { throw new Error("Must pass bytes array or opts object with size, md5hash, and crc32c"); } - if (incomingMetadata) { - this.update(incomingMetadata); - } - this.deleteFieldsSetAsNull(); this.setDownloadTokensFromCustomMetadata(); } - asRulesResource(proposedChanges?: RulesResourceMetadataOverrides): RulesResourceMetadata { - let rulesResource: RulesResourceMetadata = { - name: this.name, - bucket: this.bucket, - generation: this.generation, - metageneration: this.metageneration, - size: this.size, - timeCreated: this.timeCreated, - updated: this.updated, - md5Hash: this.md5Hash, - crc32c: this.crc32c, - etag: this.etag, - contentDisposition: this.contentDisposition, - contentEncoding: this.contentEncoding, - contentType: this.contentType, - metadata: this.customMetadata || {}, - }; + /** Creates a deep copy of a StoredFileMetadata. */ + clone(): StoredFileMetadata { + const clone = new StoredFileMetadata( + { + name: this.name, + bucket: this.bucket, + generation: this.generation, + metageneration: this.metageneration, + contentType: this.contentType, + storageClass: this.storageClass, + size: this.size, + md5Hash: this.md5Hash, + contentEncoding: this.contentEncoding, + contentDisposition: this.contentDisposition, + contentLanguage: this.contentLanguage, + cacheControl: this.cacheControl, + customTime: this.customTime, + crc32c: this.crc32c, + etag: this.etag, + downloadTokens: this.downloadTokens, + customMetadata: this.customMetadata, + }, + this._cloudFunctions, + ); + clone.timeCreated = this.timeCreated; + clone.updated = this.updated; + return clone; + } + asRulesResource(proposedChanges?: IncomingMetadata): RulesResourceMetadata { + const proposedMetadata: StoredFileMetadata = this.clone(); if (proposedChanges) { - if (proposedChanges.md5Hash !== rulesResource.md5Hash) { - // Step the generation forward and reset values - rulesResource.generation = Date.now(); - rulesResource.metageneration = 1; - rulesResource.timeCreated = new Date(); - rulesResource.updated = rulesResource.timeCreated; - } else { - // Otherwise this was just a metadata change - rulesResource.metageneration++; - } - - rulesResource = { - ...rulesResource, - ...proposedChanges, - }; + proposedMetadata.update(proposedChanges, /* shouldTrigger = */ false); } - - return rulesResource; + return { + name: proposedMetadata.name, + bucket: proposedMetadata.bucket, + generation: proposedMetadata.generation, + metageneration: proposedMetadata.metageneration, + size: proposedMetadata.size, + timeCreated: proposedMetadata.timeCreated, + updated: proposedMetadata.updated, + md5Hash: proposedMetadata.md5Hash, + crc32c: proposedMetadata.crc32c, + etag: proposedMetadata.etag, + contentDisposition: proposedMetadata.contentDisposition, + contentEncoding: proposedMetadata.contentEncoding, + contentType: proposedMetadata.contentType, + metadata: proposedMetadata.customMetadata || {}, + }; } private setDownloadTokensFromCustomMetadata() { @@ -138,8 +154,10 @@ export class StoredFileMetadata { if (this.customMetadata.firebaseStorageDownloadTokens) { this.downloadTokens = [ - ...this.downloadTokens, - ...this.customMetadata.firebaseStorageDownloadTokens.split(","), + ...new Set([ + ...this.downloadTokens, + ...this.customMetadata.firebaseStorageDownloadTokens.split(","), + ]), ]; delete this.customMetadata.firebaseStorageDownloadTokens; } @@ -170,51 +188,64 @@ export class StoredFileMetadata { } } - update(incoming: IncomingMetadata): void { - if (incoming.contentDisposition) { - this.contentDisposition = incoming.contentDisposition; + // IncomingMetadata fields are set to `null` by clients to unset the metadata fields. + // If they are undefined in IncomingMetadata, then the fields should be ignored. + update(incoming: IncomingMetadata, shouldTrigger = true): void { + if (incoming.contentDisposition !== undefined) { + this.contentDisposition = + incoming.contentDisposition === null ? undefined : incoming.contentDisposition; } - if (incoming.contentType) { - this.contentType = incoming.contentType; + if (incoming.contentType !== undefined) { + this.contentType = incoming.contentType === null ? undefined : incoming.contentType; } - if (incoming.metadata) { - this.customMetadata = incoming.metadata; + if (incoming.contentLanguage !== undefined) { + this.contentLanguage = + incoming.contentLanguage === null ? undefined : incoming.contentLanguage; } - if (incoming.contentLanguage) { - this.contentLanguage = incoming.contentLanguage; + if (incoming.contentEncoding !== undefined) { + this.contentEncoding = + incoming.contentEncoding === null ? undefined : incoming.contentEncoding; } - if (incoming.contentEncoding) { - this.contentEncoding = incoming.contentEncoding; + if (incoming.cacheControl !== undefined) { + this.cacheControl = incoming.cacheControl === null ? undefined : incoming.cacheControl; } - if (this.generation) { - this.generation++; + if (incoming.metadata !== undefined) { + if (incoming.metadata === null) { + this.customMetadata = undefined; + } else { + this.customMetadata = this.customMetadata || {}; + for (const [k, v] of Object.entries(incoming.metadata)) { + // Clients can set custom metadata fields to null to unset them. + if (v === null) { + delete this.customMetadata[k]; + } else { + // Convert all values to strings + this.customMetadata[k] = String(v); + } + } + // Clear out custom metadata if there are no more keys. + if (Object.keys(this.customMetadata).length === 0) { + this.customMetadata = undefined; + } + } } + this.metageneration++; this.updated = new Date(); - - if (incoming.cacheControl) { - this.cacheControl = incoming.cacheControl; - } - this.setDownloadTokensFromCustomMetadata(); - this.deleteFieldsSetAsNull(); - - this._cloudFunctions.dispatch("metadataUpdate", new CloudStorageObjectMetadata(this)); - } - - addDownloadToken(): void { - if (!this.downloadTokens.length) { - this.downloadTokens.push(uuid.v4()); - return; + if (shouldTrigger) { + this._cloudFunctions.dispatch("metadataUpdate", new CloudStorageObjectMetadata(this)); } + } - this.downloadTokens = [...this.downloadTokens, uuid.v4()]; - this.update({}); + addDownloadToken(shouldTrigger = true): void { + this.downloadTokens = [...(this.downloadTokens || []), uuid.v4()]; + this.update({}, shouldTrigger); } deleteDownloadToken(token: string): void { @@ -222,11 +253,12 @@ export class StoredFileMetadata { return; } - const remainingTokens = this.downloadTokens.filter((t) => t != token); + const remainingTokens = this.downloadTokens.filter((t) => t !== token); this.downloadTokens = remainingTokens; - if (remainingTokens.length == 0) { + if (remainingTokens.length === 0) { // if empty after deleting, always add a new token. - this.addDownloadToken(); + // shouldTrigger is false as it's taken care of in the subsequent update + this.addDownloadToken(/* shouldTrigger = */ false); } this.update({}); } @@ -246,7 +278,7 @@ export class StoredFileMetadata { return value; }, - 2 + 2, ); } } @@ -262,19 +294,20 @@ export interface RulesResourceMetadata { md5Hash: string; crc32c: string; etag: string; - contentDisposition: string; - contentEncoding: string; - contentType: string; + contentDisposition?: string; + contentEncoding?: string; + contentType?: string; metadata: { [s: string]: string }; } export interface IncomingMetadata { - contentType?: string; - contentLanguage?: string; - contentEncoding?: string; - contentDisposition?: string; - cacheControl?: string; - metadata?: { [s: string]: string }; + name?: string; + contentType?: string | null; + contentLanguage?: string | null; + contentEncoding?: string | null; + contentDisposition?: string | null; + cacheControl?: string | null; + metadata?: { [s: string]: string | null } | null; } export class OutgoingFirebaseMetadata { @@ -282,45 +315,45 @@ export class OutgoingFirebaseMetadata { bucket: string; generation: string; metageneration: string; - contentType: string; + contentType?: string; timeCreated: string; updated: string; storageClass: string; size: string; md5Hash: string; - contentEncoding: string; - contentDisposition: string; + contentEncoding?: string; + contentDisposition?: string; contentLanguage?: string; cacheControl?: string; crc32c: string; etag: string; downloadTokens: string; - metadata: object | undefined; - - constructor(md: StoredFileMetadata) { - this.name = md.name; - this.bucket = md.bucket; - this.generation = md.generation.toString(); - this.metageneration = md.metageneration.toString(); - this.contentType = md.contentType; - this.timeCreated = toSerializedDate(md.timeCreated); - this.updated = toSerializedDate(md.updated); - this.storageClass = md.storageClass; - this.size = md.size.toString(); - this.md5Hash = md.md5Hash; - this.crc32c = md.crc32c; - this.etag = md.etag; - this.downloadTokens = md.downloadTokens.join(","); - this.contentEncoding = md.contentEncoding; - this.contentDisposition = md.contentDisposition; - this.metadata = md.customMetadata; - this.contentLanguage = md.contentLanguage; - this.cacheControl = md.cacheControl; + metadata?: object; + + constructor(metadata: StoredFileMetadata) { + this.name = metadata.name; + this.bucket = metadata.bucket; + this.generation = metadata.generation.toString(); + this.metageneration = metadata.metageneration.toString(); + this.contentType = metadata.contentType; + this.timeCreated = toSerializedDate(metadata.timeCreated); + this.updated = toSerializedDate(metadata.updated); + this.storageClass = metadata.storageClass; + this.size = metadata.size.toString(); + this.md5Hash = metadata.md5Hash; + this.crc32c = metadata.crc32c; + this.etag = metadata.etag; + this.downloadTokens = metadata.downloadTokens.join(","); + this.contentEncoding = metadata.contentEncoding || "identity"; + this.contentDisposition = metadata.contentDisposition; + this.metadata = metadata.customMetadata; + this.contentLanguage = metadata.contentLanguage; + this.cacheControl = metadata.cacheControl; } } export class CloudStorageBucketMetadata { - kind = "#storage/bucket"; + kind = "storage#bucket"; selfLink: string; id: string; name: string; @@ -336,9 +369,11 @@ export class CloudStorageBucketMetadata { constructor(id: string) { this.name = id; this.id = id; - this.selfLink = `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/v1/b/${this.id}`; + + const selfLink = EmulatorRegistry.url(Emulators.STORAGE); + selfLink.pathname = `/v1/b/${this.id}`; + this.selfLink = selfLink.toString(); + this.timeCreated = toSerializedDate(new Date()); this.updated = this.timeCreated; this.projectNumber = "000000000000"; @@ -346,7 +381,7 @@ export class CloudStorageBucketMetadata { this.location = "US"; this.storageClass = "STANDARD"; this.etag = "===="; - this.locationType = "mutli-region"; + this.locationType = "multi-region"; } } @@ -361,17 +396,17 @@ export class CloudStorageObjectAccessControlMetadata { public role: string, public entity: string, public bucket: string, - public etag: string + public etag: string, ) {} } export class CloudStorageObjectMetadata { - kind = "#storage#object"; + kind = "storage#object"; name: string; bucket: string; generation: string; metageneration: string; - contentType: string; + contentType?: string; timeCreated: string; updated: string; storageClass: string; @@ -381,38 +416,41 @@ export class CloudStorageObjectMetadata { etag: string; metadata?: { [s: string]: string }; contentLanguage?: string; + contentDisposition?: string; cacheControl?: string; + contentEncoding?: string; customTime?: string; id: string; timeStorageClassUpdated: string; selfLink: string; mediaLink: string; - constructor(md: StoredFileMetadata) { - this.name = md.name; - this.bucket = md.bucket; - this.generation = md.generation.toString(); - this.metageneration = md.metageneration.toString(); - this.contentType = md.contentType; - this.timeCreated = toSerializedDate(md.timeCreated); - this.updated = toSerializedDate(md.updated); - this.storageClass = md.storageClass; - this.size = md.size.toString(); - this.md5Hash = md.md5Hash; - this.etag = md.etag; + constructor(metadata: StoredFileMetadata) { + this.name = metadata.name; + this.bucket = metadata.bucket; + this.generation = metadata.generation.toString(); + this.metageneration = metadata.metageneration.toString(); + this.contentType = metadata.contentType; + this.contentDisposition = metadata.contentDisposition; + this.timeCreated = toSerializedDate(metadata.timeCreated); + this.updated = toSerializedDate(metadata.updated); + this.storageClass = metadata.storageClass; + this.size = metadata.size.toString(); + this.md5Hash = metadata.md5Hash; + this.etag = metadata.etag; this.metadata = {}; - if (Object.keys(md.customMetadata || {})) { + if (Object.keys(metadata.customMetadata || {})) { this.metadata = { ...this.metadata, - ...md.customMetadata, + ...metadata.customMetadata, }; } - if (md.downloadTokens.length) { + if (metadata.downloadTokens.length) { this.metadata = { ...this.metadata, - firebaseStorageDownloadTokens: md.downloadTokens.join(","), + firebaseStorageDownloadTokens: metadata.downloadTokens.join(","), }; } @@ -420,31 +458,43 @@ export class CloudStorageObjectMetadata { delete this.metadata; } - if (md.contentLanguage) { - this.contentLanguage = md.contentLanguage; + if (metadata.contentLanguage) { + this.contentLanguage = metadata.contentLanguage; + } + + if (metadata.cacheControl) { + this.cacheControl = metadata.cacheControl; } - if (md.cacheControl) { - this.cacheControl = md.cacheControl; + if (metadata.contentDisposition) { + this.contentDisposition = metadata.contentDisposition; } - if (md.customTime) { - this.customTime = toSerializedDate(md.customTime); + if (metadata.contentEncoding) { + this.contentEncoding = metadata.contentEncoding; } - // I'm not sure why but @google-cloud/storage calls .substr(4) on this value, so we need to pad it. - this.crc32c = "----" + Buffer.from([md.crc32c]).toString("base64"); - - this.timeStorageClassUpdated = toSerializedDate(md.timeCreated); - this.id = `${md.bucket}/${md.name}/${md.generation}`; - this.selfLink = `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/storage/v1/b/${md.bucket}/o/${encodeURIComponent(md.name)}`; - this.mediaLink = `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${ - EmulatorRegistry.getInfo(Emulators.STORAGE)?.port - }/download/storage/v1/b/${md.bucket}/o/${encodeURIComponent(md.name)}?generation=${ - md.generation - }&alt=media`; + if (metadata.customTime) { + this.customTime = toSerializedDate(metadata.customTime); + } + + this.crc32c = crc32cToString(metadata.crc32c); + + this.timeStorageClassUpdated = toSerializedDate(metadata.timeCreated); + this.id = `${metadata.bucket}/${metadata.name}/${metadata.generation}`; + + const selfLink = EmulatorRegistry.url(Emulators.STORAGE); + selfLink.pathname = `/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent(metadata.name)}`; + this.selfLink = selfLink.toString(); + + const mediaLink = EmulatorRegistry.url(Emulators.STORAGE); + mediaLink.pathname = `/download/storage/v1/b/${metadata.bucket}/o/${encodeURIComponent( + metadata.name, + )}`; + mediaLink.searchParams.set("generation", metadata.generation.toString()); + mediaLink.searchParams.set("alt", "media"); + + this.mediaLink = mediaLink.toString(); } } @@ -455,16 +505,17 @@ export class CloudStorageObjectMetadata { * @return the formatted date. */ export function toSerializedDate(d: Date): string { - const day = `${d.getFullYear()}-${(d.getMonth() + 1) + const day = `${d.getUTCFullYear()}-${(d.getUTCMonth() + 1).toString().padStart(2, "0")}-${d + .getUTCDate() .toString() - .padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`; - const time = `${d.getHours().toString().padStart(2, "0")}:${d - .getMinutes() + .padStart(2, "0")}`; + const time = `${d.getUTCHours().toString().padStart(2, "0")}:${d + .getUTCMinutes() .toString() - .padStart(2, "0")}:${d - .getSeconds() + .padStart(2, "0")}:${d.getUTCSeconds().toString().padStart(2, "0")}.${d + .getUTCMilliseconds() .toString() - .padStart(2, "0")}.${d.getMilliseconds().toString().padStart(3, "0")}`; + .padStart(3, "0")}`; return `${day}T${time}Z`; } @@ -473,3 +524,10 @@ function generateMd5Hash(bytes: Buffer): string { hash.update(bytes); return hash.digest("base64"); } + +function generateETag(generation: number, metadatageneration: number): string { + const hash = crypto.createHash("sha1"); + hash.update(`${generation}/${metadatageneration}`); + // Trim padding + return hash.digest("base64").slice(0, -1); +} diff --git a/src/emulator/storage/multipart.spec.ts b/src/emulator/storage/multipart.spec.ts new file mode 100644 index 00000000000..7131a184261 --- /dev/null +++ b/src/emulator/storage/multipart.spec.ts @@ -0,0 +1,145 @@ +import { expect } from "chai"; +import { parseObjectUploadMultipartRequest } from "./multipart"; +import { randomBytes } from "crypto"; + +describe("Storage Multipart Request Parser", () => { + const CONTENT_TYPE_HEADER = "multipart/related; boundary=b1d5b2e3-1845-4338-9400-6ac07ce53c1e"; + const BODY = Buffer.from(`--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +Content-Type: application/json\r +\r +{"contentType":"text/plain"}\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +Content-Type: text/plain\r +\r +hello there! +\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r +`); + + describe("#parseObjectUploadMultipartRequest()", () => { + it("parses an upload object multipart request successfully", () => { + const { metadataRaw, dataRaw } = parseObjectUploadMultipartRequest(CONTENT_TYPE_HEADER, BODY); + + expect(metadataRaw).to.equal('{"contentType":"text/plain"}'); + expect(dataRaw.toString()).to.equal("hello there!\n"); + }); + + it("parses an upload object multipart request with non utf-8 data successfully", () => { + const bodyPart1 = Buffer.from(`--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +Content-Type: application/json\r +\r +{"contentType":"text/plain"}\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +Content-Type: text/plain\r +\r +`); + const data = Buffer.concat( + [Buffer.from(randomBytes(100)), Buffer.from("\r\n"), Buffer.from(randomBytes(100))], + 202, + ); + const bodyPart2 = Buffer.from(`\r\n--b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r\n`); + const body = Buffer.concat([bodyPart1, data, bodyPart2]); + + const { dataRaw } = parseObjectUploadMultipartRequest(CONTENT_TYPE_HEADER, body); + + expect(dataRaw.byteLength).to.equal(data.byteLength); + }); + + it("parses an upload object multipart request with lowercase content-type", () => { + const body = Buffer.from(`--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +content-type: application/json\r +\r +{"contentType":"text/plain"}\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +content-type: text/plain\r +\r +hello there! +\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r +`); + + const { metadataRaw, dataRaw } = parseObjectUploadMultipartRequest(CONTENT_TYPE_HEADER, body); + + expect(metadataRaw).to.equal('{"contentType":"text/plain"}'); + expect(dataRaw.toString()).to.equal("hello there!\n"); + }); + + it("fails to parse with invalid Content-Type value", () => { + const invalidContentTypeHeader = "blah"; + expect(() => parseObjectUploadMultipartRequest(invalidContentTypeHeader, BODY)).to.throw( + "Bad content type.", + ); + }); + + it("fails to parse with invalid boundary value", () => { + const invalidContentTypeHeader = "multipart/related; boundary="; + expect(() => parseObjectUploadMultipartRequest(invalidContentTypeHeader, BODY)).to.throw( + "Bad content type.", + ); + }); + + it("parses an upload object multipart request with additional quotes in the boundary value", () => { + const contentTypeHeaderWithDoubleQuotes = `multipart/related; boundary="b1d5b2e3-1845-4338-9400-6ac07ce53c1e"`; + + let { metadataRaw, dataRaw } = parseObjectUploadMultipartRequest( + contentTypeHeaderWithDoubleQuotes, + BODY, + ); + + expect(metadataRaw).to.equal('{"contentType":"text/plain"}'); + expect(dataRaw.toString()).to.equal("hello there!\n"); + + const contentTypeHeaderWithSingleQuotes = `multipart/related; boundary='b1d5b2e3-1845-4338-9400-6ac07ce53c1e'`; + + ({ metadataRaw, dataRaw } = parseObjectUploadMultipartRequest( + contentTypeHeaderWithSingleQuotes, + BODY, + )); + + expect(metadataRaw).to.equal('{"contentType":"text/plain"}'); + expect(dataRaw.toString()).to.equal("hello there!\n"); + }); + + it("fails to parse when body has wrong number of parts", () => { + const invalidBody = Buffer.from(`--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +Content-Type: application/json\r +\r +{"contentType":"text/plain"}\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r +`); + expect(() => parseObjectUploadMultipartRequest(CONTENT_TYPE_HEADER, invalidBody)).to.throw( + "Unexpected number of parts", + ); + }); + + it("fails to parse when body part has invalid content type", () => { + const invalidBody = Buffer.from(`--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +bogus content type\r +\r +{"contentType":"text/plain"}\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +bogus content type\r +\r +hello there! +\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r +`); + expect(() => parseObjectUploadMultipartRequest(CONTENT_TYPE_HEADER, invalidBody)).to.throw( + "Missing content type.", + ); + }); + + it("fails to parse when body part is malformed", () => { + const invalidBody = Buffer.from(`--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +\r +{"contentType":"text/plain"}\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r +\r +--b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r +`); + expect(() => parseObjectUploadMultipartRequest(CONTENT_TYPE_HEADER, invalidBody)).to.throw( + "Failed to parse multipart request body part", + ); + }); + }); +}); diff --git a/src/emulator/storage/multipart.ts b/src/emulator/storage/multipart.ts new file mode 100644 index 00000000000..bb0850a5275 --- /dev/null +++ b/src/emulator/storage/multipart.ts @@ -0,0 +1,142 @@ +/** + * Represents a parsed multipart form body for an upload object request. + * + * Note: This class and others in files deal directly with buffers as + * converting to String can append unwanted encoding data to the blob data + * passed in the original request. + */ +export type ObjectUploadMultipartData = { + metadataRaw: string; + dataRaw: Buffer; +}; + +/** + * Represents a parsed multipart request body. Request bodies can have an + * arbitrary number of parts. + */ +type MultipartRequestBody = MultipartRequestBodyPart[]; + +const LINE_SEPARATOR = `\r\n`; + +/** + * Returns an array of Buffers constructed by splitting a Buffer on a delimiter. + * @param maxResults Returns at most this many results. Any slices remaining in the + * original buffer will be returned as a single Buffer at the end + */ +function splitBufferByDelimiter(buffer: Buffer, delimiter: string, maxResults = -1): Buffer[] { + // Iterate through delimited slices and save to separate Buffers + let offset = 0; + let nextDelimiterIndex = buffer.indexOf(delimiter, offset); + const bufferParts: Buffer[] = []; + while (nextDelimiterIndex !== -1) { + if (maxResults === 0) { + return bufferParts; + } else if (maxResults === 1) { + // Save the rest of the buffer as one slice and return. + bufferParts.push(Buffer.from(buffer.slice(offset))); + return bufferParts; + } + bufferParts.push(Buffer.from(buffer.slice(offset, nextDelimiterIndex))); + offset = nextDelimiterIndex + delimiter.length; + nextDelimiterIndex = buffer.indexOf(delimiter, offset); + maxResults -= 1; + } + bufferParts.push(Buffer.from(buffer.slice(offset))); + return bufferParts; +} + +/** + * Parses a multipart request body buffer into a {@link MultipartRequestBody}. + * @param boundaryId the boundary id of the multipart request + * @param body multipart request body as a Buffer + */ +function parseMultipartRequestBody(boundaryId: string, body: Buffer): MultipartRequestBody { + // strip additional surrounding single and double quotes, cloud sdks have additional quote here + const cleanBoundaryId = boundaryId.replace(/^["'](.+(?=["']$))["']$/, "$1"); + const boundaryString = `--${cleanBoundaryId}`; + const bodyParts = splitBufferByDelimiter(body, boundaryString).map((buf) => { + // Remove the \r\n and the beginning of each part left from the boundary line. + return Buffer.from(buf.slice(2)); + }); + // A valid split request body should have two extra Buffers, one at the beginning and end. + const parsedParts: MultipartRequestBodyPart[] = []; + for (const bodyPart of bodyParts.slice(1, bodyParts.length - 1)) { + parsedParts.push(parseMultipartRequestBodyPart(bodyPart)); + } + return parsedParts; +} + +/** + * Represents a single boundary-delineated multipart request body part, + * Ex: """Content-Type: application/json\r + * \r + * {"contentType":"text/plain"}\r + * """ + */ +type MultipartRequestBodyPart = { + // From the example above: "Content-Type: application/json" + contentTypeRaw: string; + // From the example above: '{"contentType":"text/plain"}' + dataRaw: Buffer; +}; + +/** + * Parses a string into a {@link MultipartRequestBodyPart}. We expect 3 sections + * delineated by '\r\n': + * 1: content type + * 2: white space + * 3: free form data + * @param bodyPart a multipart request body part as a Buffer + */ +function parseMultipartRequestBodyPart(bodyPart: Buffer): MultipartRequestBodyPart { + // The free form data section may have \r\n data in it so glob it together rather than + // splitting the entire body part buffer. + const sections = splitBufferByDelimiter(bodyPart, LINE_SEPARATOR, /* maxResults = */ 3); + + const contentTypeRaw = sections[0].toString().toLowerCase(); + if (!contentTypeRaw.startsWith("content-type: ")) { + throw new Error(`Failed to parse multipart request body part. Missing content type.`); + } + + // Remove trailing '\r\n' from the last line since splitBufferByDelimiter will not with + // maxResults set. + const dataRaw = Buffer.from(sections[2]).slice(0, sections[2].byteLength - LINE_SEPARATOR.length); + return { contentTypeRaw, dataRaw }; +} + +/** + * Parses a multipart form request for a file upload into its parts. + * @param contentTypeHeader value of ContentType header passed in request. + * Example: "multipart/related; boundary=b1d5b2e3-1845-4338-9400-6ac07ce53c1e" + * @param body string value of the body of the multipart request. + * Example: """--b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r + * Content-Type: application/json\r + * \r + * {"contentType":"text/plain"}\r + * --b1d5b2e3-1845-4338-9400-6ac07ce53c1e\r + * Content-Type: text/plain\r + * \r + * �ZDn�QF�&�\r + * --b1d5b2e3-1845-4338-9400-6ac07ce53c1e--\r + * """ + */ +export function parseObjectUploadMultipartRequest( + contentTypeHeader: string, + body: Buffer, +): ObjectUploadMultipartData { + if (!contentTypeHeader.startsWith("multipart/related")) { + throw new Error(`Bad content type. ${contentTypeHeader}`); + } + const boundaryId = contentTypeHeader.split("boundary=")[1]; + if (!boundaryId) { + throw new Error(`Bad content type. ${contentTypeHeader}`); + } + const parsedBody = parseMultipartRequestBody(boundaryId, body); + if (parsedBody.length !== 2) { + throw new Error(`Unexpected number of parts in request body`); + } + return { + metadataRaw: parsedBody[0].dataRaw.toString(), + dataRaw: Buffer.from(parsedBody[1].dataRaw), + }; +} diff --git a/src/emulator/storage/persistence.spec.ts b/src/emulator/storage/persistence.spec.ts new file mode 100644 index 00000000000..5bf165cc0c5 --- /dev/null +++ b/src/emulator/storage/persistence.spec.ts @@ -0,0 +1,55 @@ +import { expect } from "chai"; +import { tmpdir } from "os"; +import * as fs from "fs"; + +import { v4 as uuidV4 } from "uuid"; +import { Persistence } from "./persistence"; + +describe("Persistence", () => { + const testDir = `${tmpdir()}/${uuidV4()}`; + const _persistence = new Persistence(testDir); + after(async () => { + await _persistence.deleteAll(); + }); + + describe("#deleteFile()", () => { + it("should delete files", () => { + const filename = `${uuidV4()}%2F${uuidV4()}`; + _persistence.appendBytes(filename, Buffer.from("hello world")); + + _persistence.deleteFile(filename); + expect(() => _persistence.readBytes(filename, 10)).to.throw(); + }); + }); + + describe("#readBytes()", () => { + it("should read existing files", () => { + const filename = `${uuidV4()}%2F${uuidV4()}`; + const data = Buffer.from("hello world"); + + _persistence.appendBytes(filename, data); + expect(_persistence.readBytes(filename, data.byteLength).toString()).to.equal("hello world"); + }); + it("should handle really long filename read existing files", () => { + const filename = `${uuidV4()}%2F%${"long".repeat(180)}${uuidV4()}`; + const data = Buffer.from("hello world"); + + _persistence.appendBytes(filename, data); + expect(_persistence.readBytes(filename, data.byteLength).toString()).to.equal("hello world"); + }); + }); + + describe("#copyFromExternalPath()", () => { + it("should copy files existing files", () => { + const data = Buffer.from("hello world"); + const externalFilename = `${uuidV4()}%2F${uuidV4()}`; + const externalFilePath = `${testDir}/${externalFilename}`; + fs.appendFileSync(externalFilePath, data); + + const filename = `${uuidV4()}%2F${uuidV4()}`; + + _persistence.copyFromExternalPath(externalFilePath, filename); + expect(_persistence.readBytes(filename, data.byteLength).toString()).to.equal("hello world"); + }); + }); +}); diff --git a/src/emulator/storage/persistence.ts b/src/emulator/storage/persistence.ts new file mode 100644 index 00000000000..d47c2ee0d5f --- /dev/null +++ b/src/emulator/storage/persistence.ts @@ -0,0 +1,97 @@ +import { openSync, closeSync, readSync, unlinkSync, mkdirSync } from "fs"; +import { rimraf } from "rimraf"; +import * as fs from "fs"; +import * as fse from "fs-extra"; +import * as path from "path"; +import * as uuid from "uuid"; + +/** + * Helper for disk I/O operations. + * Assigns a unique identifier to each file and stores it on disk based on that identifier + */ +export class Persistence { + private _dirPath!: string; + // Mapping from emulator filePaths to unique identifiers on disk + private _diskPathMap: Map = new Map(); + constructor(dirPath: string) { + this.reset(dirPath); + } + + public reset(dirPath: string) { + this._dirPath = dirPath; + mkdirSync(dirPath, { + recursive: true, + }); + this._diskPathMap = new Map(); + } + + public get dirPath(): string { + return this._dirPath; + } + + appendBytes(fileName: string, bytes: Buffer): string { + if (!this._diskPathMap.has(fileName)) { + this._diskPathMap.set(fileName, this.generateNewDiskName()); + } + const filepath = this.getDiskPath(fileName); + + fs.appendFileSync(filepath, bytes); + return filepath; + } + + readBytes(fileName: string, size: number, fileOffset?: number): Buffer { + let fd; + try { + fd = openSync(this.getDiskPath(fileName), "r"); + const buf = Buffer.alloc(size); + const offset = fileOffset && fileOffset > 0 ? fileOffset : 0; + readSync(fd, buf, 0, size, offset); + return buf; + } finally { + if (fd) { + closeSync(fd); + } + } + } + + deleteFile(fileName: string, failSilently = false): void { + try { + unlinkSync(this.getDiskPath(fileName)); + } catch (err: any) { + if (!failSilently) { + throw err; + } + } + this._diskPathMap.delete(fileName); + } + + async deleteAll(): Promise { + await rimraf(this._dirPath); + this._diskPathMap = new Map(); + return; + } + + renameFile(oldName: string, newName: string): void { + const oldNameId = this.getDiskFileName(oldName); + this._diskPathMap.set(newName, oldNameId); + this._diskPathMap.delete(oldName); + } + + getDiskPath(fileName: string): string { + const shortenedDiskPath = this.getDiskFileName(fileName); + return path.join(this._dirPath, encodeURIComponent(shortenedDiskPath)); + } + + getDiskFileName(fileName: string): string { + return this._diskPathMap.get(fileName)!; + } + + copyFromExternalPath(sourcePath: string, newName: string): void { + this._diskPathMap.set(newName, this.generateNewDiskName()); + fse.copyFileSync(sourcePath, this.getDiskPath(newName)); + } + + private generateNewDiskName(): string { + return uuid.v4(); + } +} diff --git a/src/emulator/storage/rfc.ts b/src/emulator/storage/rfc.ts new file mode 100644 index 00000000000..c548df5d64a --- /dev/null +++ b/src/emulator/storage/rfc.ts @@ -0,0 +1,12 @@ +/** + * Adapted from: + * - https://datatracker.ietf.org/doc/html/rfc5987 + * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#examples + * + * @returns RFC5987 encoded string + */ +export function encodeRFC5987(str: string): string { + return encodeURIComponent(str) + .replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`) + .replace(/%(7C|60|5E)/g, (str, hex) => String.fromCharCode(parseInt(hex, 16))); +} diff --git a/src/emulator/storage/rules/config.spec.ts b/src/emulator/storage/rules/config.spec.ts new file mode 100644 index 00000000000..dc57cfd431f --- /dev/null +++ b/src/emulator/storage/rules/config.spec.ts @@ -0,0 +1,138 @@ +import { expect } from "chai"; + +import { Options } from "../../../options"; +import { RC } from "../../../rc"; +import { getStorageRulesConfig } from "./config"; +import { createTmpDir, StorageRulesFiles } from "../../testing/fixtures"; +import { FirebaseError } from "../../../error"; +import { Persistence } from "../persistence"; +import { RulesConfig } from ".."; +import { SourceFile } from "./types"; + +const PROJECT_ID = "test-project"; + +describe("Storage Rules Config", () => { + const tmpDir = createTmpDir("storage-files"); + const persistence = new Persistence(tmpDir); + const resolvePath = (fileName: string) => fileName; + + it("should parse rules config for single target", () => { + const rulesFile = "storage.rules"; + const rulesContent = Buffer.from(StorageRulesFiles.readWriteIfTrue.content); + const path = persistence.appendBytes(rulesFile, rulesContent); + + const config = getOptions({ + data: { storage: { rules: path } }, + path: resolvePath, + }); + const result = getStorageRulesConfig(PROJECT_ID, config) as SourceFile; + + expect(result.name).to.equal(path); + expect(result.content).to.contain("allow read, write: if true"); + }); + + it("should use default config for project IDs using demo- prefix if no rules file exists", () => { + const config = getOptions({ + data: {}, + path: resolvePath, + }); + const result = getStorageRulesConfig("demo-projectid", config) as SourceFile; + + expect(result.name).to.contain("templates/emulators/default_storage.rules"); + expect(result.content).to.contain("allow read, write;"); + }); + + it("should use provided config for project IDs using demo- prefix if the provided config exists", () => { + const rulesFile = "storage.rules"; + const rulesContent = Buffer.from(StorageRulesFiles.readWriteIfTrue.content); + const path = persistence.appendBytes(rulesFile, rulesContent); + + const config = getOptions({ + data: { storage: { rules: path } }, + path: resolvePath, + }); + const result = getStorageRulesConfig("demo-projectid", config) as SourceFile; + + expect(result.name).to.equal(path); + expect(result.content).to.contain("allow read, write: if true"); + }); + + it("should parse rules file for multiple targets", () => { + const mainRulesContent = Buffer.from(StorageRulesFiles.readWriteIfTrue.content); + const otherRulesContent = Buffer.from(StorageRulesFiles.readWriteIfAuth.content); + const mainRulesPath = persistence.appendBytes("storage_main.rules", mainRulesContent); + const otherRulesPath = persistence.appendBytes("storage_other.rules", otherRulesContent); + + const config = getOptions({ + data: { + storage: [ + { target: "main", rules: mainRulesPath }, + { target: "other", rules: otherRulesPath }, + ], + }, + path: resolvePath, + }); + config.rc.applyTarget(PROJECT_ID, "storage", "main", ["bucket_0", "bucket_1"]); + config.rc.applyTarget(PROJECT_ID, "storage", "other", ["bucket_2"]); + + const result = getStorageRulesConfig(PROJECT_ID, config) as RulesConfig[]; + + expect(result.length).to.equal(3); + + expect(result[0].resource).to.eql("bucket_0"); + expect(result[0].rules.name).to.equal(mainRulesPath); + expect(result[0].rules.content).to.contain("allow read, write: if true"); + + expect(result[1].resource).to.eql("bucket_1"); + expect(result[1].rules.name).to.equal(mainRulesPath); + expect(result[1].rules.content).to.contain("allow read, write: if true"); + + expect(result[2].resource).to.eql("bucket_2"); + expect(result[2].rules.name).to.equal(otherRulesPath); + expect(result[2].rules.content).to.contain("allow read, write: if request.auth!=null"); + }); + + it("should throw FirebaseError when storage config is missing", () => { + const config = getOptions({ data: {}, path: resolvePath }); + expect(() => getStorageRulesConfig(PROJECT_ID, config)).to.throw( + FirebaseError, + "Cannot start the Storage emulator without rules file specified in firebase.json: run 'firebase init' and set up your Storage configuration", + ); + }); + + it("should throw FirebaseError when rules file is missing", () => { + const config = getOptions({ data: { storage: {} }, path: resolvePath }); + expect(() => getStorageRulesConfig(PROJECT_ID, config)).to.throw( + FirebaseError, + "Cannot start the Storage emulator without rules file specified in firebase.json: run 'firebase init' and set up your Storage configuration", + ); + }); + + it("should throw FirebaseError when rules file is invalid", () => { + const invalidFileName = "foo"; + const config = getOptions({ data: { storage: { rules: invalidFileName } }, path: resolvePath }); + expect(() => getStorageRulesConfig(PROJECT_ID, config)).to.throw( + FirebaseError, + `File not found: ${resolvePath(invalidFileName)}`, + ); + }); +}); + +function getOptions(config: any): Options { + return { + cwd: "/", + configPath: "/", + /* eslint-disable-next-line */ + config, + only: "", + except: "", + nonInteractive: false, + json: false, + interactive: false, + debug: false, + force: false, + filteredTargets: [], + rc: new RC(), + project: PROJECT_ID, + }; +} diff --git a/src/emulator/storage/rules/config.ts b/src/emulator/storage/rules/config.ts new file mode 100644 index 00000000000..5f333acd9d3 --- /dev/null +++ b/src/emulator/storage/rules/config.ts @@ -0,0 +1,86 @@ +import { RulesConfig } from ".."; +import { FirebaseError } from "../../../error"; +import { readFile } from "../../../fsutils"; +import { Options } from "../../../options"; +import { SourceFile } from "./types"; +import { Constants } from "../../constants"; +import { Emulators } from "../../types"; +import { EmulatorLogger } from "../../emulatorLogger"; +import { absoluteTemplateFilePath } from "../../../templates"; + +function getSourceFile(rules: string, options: Options): SourceFile { + const path = options.config.path(rules); + return { name: path, content: readFile(path) }; +} + +/** + * Parses rules file for each target specified in the storage config under {@link options}. + * @returns The rules file path if the storage config does not specify a target and an array + * of project resources and their corresponding rules files otherwise. + * @throws {FirebaseError} if storage config is missing or rules file is missing or invalid. + */ +export function getStorageRulesConfig( + projectId: string, + options: Options, +): SourceFile | RulesConfig[] { + const storageConfig = options.config.data.storage; + const storageLogger = EmulatorLogger.forEmulator(Emulators.STORAGE); + if (!storageConfig) { + if (Constants.isDemoProject(projectId)) { + storageLogger.logLabeled( + "BULLET", + "storage", + `Detected demo project ID "${projectId}", using a default (open) rules configuration.`, + ); + return defaultStorageRules(); + } + throw new FirebaseError( + "Cannot start the Storage emulator without rules file specified in firebase.json: run 'firebase init' and set up your Storage configuration", + ); + } + + // No target specified + if (!Array.isArray(storageConfig)) { + if (!storageConfig.rules) { + throw new FirebaseError( + "Cannot start the Storage emulator without rules file specified in firebase.json: run 'firebase init' and set up your Storage configuration", + ); + } + + return getSourceFile(storageConfig.rules, options); + } + // Multiple targets + const results: RulesConfig[] = []; + const { rc } = options; + for (const targetConfig of storageConfig) { + if (!targetConfig.target) { + throw new FirebaseError("Must supply 'target' in Storage configuration"); + } + const targets = rc.target(projectId, "storage", targetConfig.target); + if (targets.length === 0) { + // Fall back to open if this is a demo project + if (Constants.isDemoProject(projectId)) { + storageLogger.logLabeled( + "BULLET", + "storage", + `Detected demo project ID "${projectId}", using a default (open) rules configuration. Storage targets in firebase.json will be ignored.`, + ); + return defaultStorageRules(); + } + // Otherwise, requireTarget will error out + rc.requireTarget(projectId, "storage", targetConfig.target); + } + results.push( + ...rc.target(projectId, "storage", targetConfig.target).map((resource: string) => { + return { resource, rules: getSourceFile(targetConfig.rules, options) }; + }), + ); + } + return results; +} + +function defaultStorageRules(): SourceFile { + const defaultRulesPath = "emulators/default_storage.rules"; + const name = absoluteTemplateFilePath(defaultRulesPath); + return { name, content: readFile(name) }; +} diff --git a/src/emulator/storage/rules/manager.ts b/src/emulator/storage/rules/manager.ts new file mode 100644 index 00000000000..ba95df64a11 --- /dev/null +++ b/src/emulator/storage/rules/manager.ts @@ -0,0 +1,162 @@ +import * as chokidar from "chokidar"; +import { EmulatorLogger } from "../../emulatorLogger"; +import { Emulators } from "../../types"; +import { SourceFile } from "./types"; +import { StorageRulesIssues, StorageRulesRuntime, StorageRulesetInstance } from "./runtime"; +import { RulesConfig } from ".."; +import { readFile } from "../../../fsutils"; + +/** + * Keeps track of rules source file(s) and generated ruleset(s), either one for all storage + * resources or different rules for different resources. + * + * Example usage: + * + * ``` + * const rulesManager = createStorageRulesManager(initialRules); + * rulesManager.start(); + * rulesManager.stop(); + * ``` + */ +export interface StorageRulesManager { + /** Sets source file for each resource using the most recent rules. */ + start(): Promise; + + /** + * Retrieves the generated ruleset for the resource. Returns undefined if the resource is invalid + * or if the ruleset has not been generated. + */ + getRuleset(resource: string): StorageRulesetInstance | undefined; + + /** Removes listeners from all files for all managed resources. */ + stop(): Promise; +} + +/** + * Creates either a {@link DefaultStorageRulesManager} to manage rules for all resources or a + * {@link ResourceBasedStorageRulesManager} for a subset of them, keyed by resource name. + */ +export function createStorageRulesManager( + rules: SourceFile | RulesConfig[], + runtime: StorageRulesRuntime, +): StorageRulesManager { + return Array.isArray(rules) + ? new ResourceBasedStorageRulesManager(rules, runtime) + : new DefaultStorageRulesManager(rules, runtime); +} + +/** + * Maintains a {@link StorageRulesetInstance} for a given source file. Listens for changes to the + * file and updates the ruleset accordingly. + */ +class DefaultStorageRulesManager implements StorageRulesManager { + private _rules: SourceFile; + private _ruleset?: StorageRulesetInstance; + private _watcher = new chokidar.FSWatcher(); + private _logger = EmulatorLogger.forEmulator(Emulators.STORAGE); + + constructor( + _rules: SourceFile, + private _runtime: StorageRulesRuntime, + ) { + this._rules = _rules; + } + + async start(): Promise { + const issues = await this.loadRuleset(); + this.updateWatcher(this._rules.name); + return issues; + } + + getRuleset(): StorageRulesetInstance | undefined { + return this._ruleset; + } + + async stop(): Promise { + await this._watcher.close(); + } + + private updateWatcher(rulesFile: string): void { + this._watcher = chokidar + .watch(rulesFile, { persistent: true, ignoreInitial: true }) + .on("change", async () => { + // There have been some race conditions reported (on Windows) where reading the + // file too quickly after the watcher fires results in an empty file being read. + // Adding a small delay prevents that at very little cost. + await new Promise((res) => setTimeout(res, 5)); + + this._logger.logLabeled( + "BULLET", + "storage", + "Change detected, updating rules for Cloud Storage...", + ); + this._rules.content = readFile(rulesFile); + await this.loadRuleset(); + }); + } + + private async loadRuleset(): Promise { + const { ruleset, issues } = await this._runtime.loadRuleset({ files: [this._rules] }); + + if (ruleset) { + this._ruleset = ruleset; + return issues; + } + + issues.all.forEach((issue: string) => { + try { + const parsedIssue = JSON.parse(issue); + this._logger.log( + "WARN", + `${parsedIssue.description_.replace(/\.$/, "")} in ${ + parsedIssue.sourcePosition_.fileName_ + }:${parsedIssue.sourcePosition_.line_}`, + ); + } catch { + this._logger.logLabeled("WARN", "storage", issue); + } + }); + return issues; + } +} + +/** + * Maintains a mapping from storage resource to {@link DefaultStorageRulesManager} and + * directs calls to the appropriate instance. + */ +class ResourceBasedStorageRulesManager implements StorageRulesManager { + private _rulesManagers = new Map(); + + constructor( + _rulesConfig: RulesConfig[], + private _runtime: StorageRulesRuntime, + ) { + for (const { resource, rules } of _rulesConfig) { + this.createRulesManager(resource, rules); + } + } + + async start(): Promise { + const allIssues = new StorageRulesIssues(); + for (const rulesManager of this._rulesManagers.values()) { + allIssues.extend(await rulesManager.start()); + } + return allIssues; + } + + getRuleset(resource: string): StorageRulesetInstance | undefined { + return this._rulesManagers.get(resource)?.getRuleset(); + } + + async stop(): Promise { + await Promise.all( + Array.from(this._rulesManagers.values(), async (rulesManager) => await rulesManager.stop()), + ); + } + + private createRulesManager(resource: string, rules: SourceFile): DefaultStorageRulesManager { + const rulesManager = new DefaultStorageRulesManager(rules, this._runtime); + this._rulesManagers.set(resource, rulesManager); + return rulesManager; + } +} diff --git a/src/emulator/storage/rules/runtime.ts b/src/emulator/storage/rules/runtime.ts index b153bca85b5..8f1702eb3ac 100644 --- a/src/emulator/storage/rules/runtime.ts +++ b/src/emulator/storage/rules/runtime.ts @@ -1,9 +1,13 @@ import { spawn } from "cross-spawn"; import { ChildProcess } from "child_process"; import { FirebaseError } from "../../../error"; +import * as AsyncLock from "async-lock"; import { + DataLoadStatus, RulesetOperationMethod, RuntimeActionBundle, + RuntimeActionFirestoreDataRequest, + RuntimeActionFirestoreDataResponse, RuntimeActionLoadRulesetBundle, RuntimeActionLoadRulesetResponse, RuntimeActionRequest, @@ -23,9 +27,13 @@ import { downloadEmulator } from "../../download"; import * as fs from "fs-extra"; import { _getCommand, - DownloadDetails, + getDownloadDetails, handleEmulatorProcessError, } from "../../downloadableEmulators"; +import { EmulatorRegistry } from "../../registry"; + +const lock = new AsyncLock(); +const synchonizationKey = "key"; export interface RulesetVerificationOpts { file: { @@ -35,26 +43,28 @@ export interface RulesetVerificationOpts { token?: string; method: RulesetOperationMethod; path: string; + delimiter?: string; + projectId: string; } export class StorageRulesetInstance { constructor( private runtime: StorageRulesRuntime, private rulesVersion: number, - private rulesetName: string + private rulesetName: string, ) {} async verify( opts: RulesetVerificationOpts, - runtimeVariableOverrides: { [s: string]: ExpressionValue } = {} + runtimeVariableOverrides: { [s: string]: ExpressionValue } = {}, ): Promise<{ permitted?: boolean; issues: StorageRulesIssues; }> { - if (opts.method == RulesetOperationMethod.LIST && this.rulesVersion < 2) { + if (opts.method === RulesetOperationMethod.LIST && this.rulesVersion < 2) { const issues = new StorageRulesIssues(); issues.warnings.push( - "Permission denied. List operations are only allowed for rules_version='2'." + "Permission denied. List operations are only allowed for rules_version='2'.", ); return { permitted: false, @@ -71,7 +81,10 @@ export class StorageRulesetInstance { } export class StorageRulesIssues { - constructor(public errors: string[] = [], public warnings: string[] = []) {} + constructor( + public errors: string[] = [], + public warnings: string[] = [], + ) {} static fromResponse(resp: RuntimeActionResponse) { return new StorageRulesIssues(resp.errors || [], resp.warnings || []); @@ -84,6 +97,11 @@ export class StorageRulesIssues { exist(): boolean { return !!(this.errors.length || this.warnings.length); } + + extend(other: StorageRulesIssues): void { + this.errors.push(...other.errors); + this.warnings.push(...other.warnings); + } } export class StorageRulesRuntime { @@ -102,17 +120,20 @@ export class StorageRulesRuntime { return this._alive; } - async start(auto_download = true) { - const downloadDetails = DownloadDetails[Emulators.STORAGE]; + async start(autoDownload = true) { + if (this.alive) { + return; + } + const downloadDetails = getDownloadDetails(Emulators.STORAGE); const hasEmulator = fs.existsSync(downloadDetails.downloadPath); if (!hasEmulator) { - if (auto_download) { + if (autoDownload) { if (process.env.CI) { utils.logWarning( `It appears you are running in a CI environment. You can avoid downloading the ${Constants.description( - Emulators.STORAGE - )} repeatedly by caching the ${downloadDetails.opts.cacheDir} directory.` + Emulators.STORAGE, + )} repeatedly by caching the ${downloadDetails.opts.cacheDir} directory.`, ); } @@ -129,11 +150,10 @@ export class StorageRulesRuntime { stdio: ["pipe", "pipe", "pipe"], }); - this._childprocess.on("exit", (code) => { + this._childprocess.on("exit", () => { this._alive = false; - if (code !== 130 /* SIGINT */) { - throw new FirebaseError("Storage Emulator Rules runtime exited unexpectedly."); - } + this._childprocess?.removeAllListeners(); + this._childprocess = undefined; }); const startPromise = new Promise((resolve) => { @@ -152,36 +172,44 @@ export class StorageRulesRuntime { }); // This catches errors from the java process (i.e. missing jar file) - this._childprocess.stderr.on("data", (buf: Buffer) => { + this._childprocess.stderr?.on("data", (buf: Buffer) => { const error = buf.toString(); if (error.includes("jarfile")) { + EmulatorLogger.forEmulator(Emulators.STORAGE).log("ERROR", error); throw new FirebaseError( - "There was an issue starting the rules emulator, please run 'firebase setup:emulators:storage` again" + "There was an issue starting the rules emulator, please run 'firebase setup:emulators:storage` again", ); } else { EmulatorLogger.forEmulator(Emulators.STORAGE).log( "WARN", - `Unexpected rules runtime error: ${buf.toString()}` + `Unexpected rules runtime error: ${buf.toString()}`, ); } }); - this._childprocess.stdout.on("data", (buf: Buffer) => { - const serializedRuntimeActionResponse = buf.toString("UTF8").trim(); - if (serializedRuntimeActionResponse != "") { + this._childprocess.stdout?.on("data", (buf: Buffer) => { + const serializedRuntimeActionResponse = buf.toString("utf-8").trim(); + if (serializedRuntimeActionResponse !== "") { let rap; try { rap = JSON.parse(serializedRuntimeActionResponse) as RuntimeActionResponse; - } catch (err) { + } catch (err: any) { EmulatorLogger.forEmulator(Emulators.STORAGE).log( "INFO", - serializedRuntimeActionResponse + serializedRuntimeActionResponse, ); return; } - const request = this._requests[rap.id]; - if (rap.status !== "ok") { + const id = rap.id ?? rap.server_request_id; + if (id === undefined) { + console.log(`Received no ID from server response ${serializedRuntimeActionResponse}`); + return; + } + + const request = this._requests[id]; + + if (rap.status !== "ok" && !("action" in rap)) { console.warn(`[RULES] ${rap.status}: ${rap.message}`); rap.errors.forEach(console.warn.bind(console)); return; @@ -198,23 +226,39 @@ export class StorageRulesRuntime { return startPromise; } - stop() { - this._childprocess?.kill("SIGINT"); + stop(): Promise { + EmulatorLogger.forEmulator(Emulators.STORAGE).log("DEBUG", "Stopping rules runtime."); + return new Promise((resolve) => { + if (this.alive) { + this._childprocess!.on("exit", () => { + resolve(); + }); + this._childprocess?.kill("SIGINT"); + } else { + resolve(); + } + }); } - private async _sendRequest(rab: RuntimeActionBundle) { + private async _sendRequest(rab: RuntimeActionBundle, overrideId?: number) { if (!this._childprocess) { throw new FirebaseError( - "Attempted to send Cloud Storage rules request before child was ready" + "Failed to send Cloud Storage rules request due to rules runtime not available.", ); } const runtimeActionRequest: RuntimeActionRequest = { ...rab, - id: this._requestCount++, + id: overrideId ?? this._requestCount++, }; - if (this._requests[runtimeActionRequest.id]) { + // If `overrideId` is set, we are to use this ID to send to Rules. + // This happens when there is a back-and-forth interaction with Rules, + // meaning we also need to delete the old request and await the new + // response with the same ID. + if (overrideId !== undefined) { + delete this._requests[overrideId]; + } else if (this._requests[runtimeActionRequest.id]) { throw new FirebaseError("Attempted to send Cloud Storage rules request with stale id"); } @@ -225,13 +269,21 @@ export class StorageRulesRuntime { }; const serializedRequest = JSON.stringify(runtimeActionRequest); - this._childprocess?.stdin.write(serializedRequest + "\n"); + + // Added due to https://github.com/firebase/firebase-tools/issues/3915 + // Without waiting to acquire the lock and allowing the child process enough time + // (~15ms) to pipe the output back, the emulator will run into issues with + // capturing the output and resolving corresponding promises en masse. + lock.acquire(synchonizationKey, (done) => { + this._childprocess?.stdin?.write(serializedRequest + "\n"); + setTimeout(() => { + done(); + }, 15); + }); }); } - async loadRuleset( - source: Source - ): Promise<{ + async loadRuleset(source: Source): Promise<{ ruleset?: StorageRulesetInstance; issues: StorageRulesIssues; }> { @@ -245,10 +297,10 @@ export class StorageRulesRuntime { }; const response = (await this._sendRequest( - runtimeActionRequest + runtimeActionRequest, )) as RuntimeActionLoadRulesetResponse; - if (response.errors.length || response.warnings.length) { + if (response.errors.length) { return { issues: StorageRulesIssues.fromResponse(response), }; @@ -258,7 +310,7 @@ export class StorageRulesRuntime { ruleset: new StorageRulesetInstance( this, response.result.rulesVersion, - runtimeActionRequest.context.rulesetName + runtimeActionRequest.context.rulesetName, ), }; } @@ -267,7 +319,7 @@ export class StorageRulesRuntime { async verifyWithRuleset( rulesetName: string, opts: RulesetVerificationOpts, - runtimeVariableOverrides: { [s: string]: ExpressionValue } = {} + runtimeVariableOverrides: { [s: string]: ExpressionValue } = {}, ): Promise< Promise<{ permitted?: boolean; @@ -295,11 +347,31 @@ export class StorageRulesRuntime { service: "firebase.storage", path: opts.path, method: opts.method, + delimiter: opts.delimiter, variables: runtimeVariables, }, }; - const response = (await this._sendRequest(runtimeActionRequest)) as RuntimeActionVerifyResponse; + return this._completeVerifyWithRuleset(opts.projectId, runtimeActionRequest); + } + + private async _completeVerifyWithRuleset( + projectId: string, + runtimeActionRequest: RuntimeActionBundle, + overrideId?: number, + ): Promise<{ + permitted?: boolean; + issues: StorageRulesIssues; + }> { + const response = (await this._sendRequest( + runtimeActionRequest, + overrideId, + )) as RuntimeActionVerifyResponse; + + if ("context" in response) { + const dataResponse = await fetchFirestoreDocument(projectId, response); + return this._completeVerifyWithRuleset(projectId, dataResponse, response.server_request_id); + } if (!response.errors) response.errors = []; if (!response.warnings) response.warnings = []; @@ -318,16 +390,16 @@ export class StorageRulesRuntime { } function toExpressionValue(obj: any): ExpressionValue { - if (typeof obj == "string") { + if (typeof obj === "string") { return { string_value: obj }; } - if (typeof obj == "boolean") { + if (typeof obj === "boolean") { return { bool_value: obj }; } - if (typeof obj == "number") { - if (Math.floor(obj) == obj) { + if (typeof obj === "number") { + if (Math.floor(obj) === obj) { return { int_value: obj }; } else { return { float_value: obj }; @@ -358,11 +430,11 @@ function toExpressionValue(obj: any): ExpressionValue { if (obj == null) { return { - null_value: 0, + null_value: null, }; } - if (typeof obj == "object") { + if (typeof obj === "object") { const fields: { [s: string]: ExpressionValue } = {}; Object.keys(obj).forEach((key: string) => { fields[key] = toExpressionValue(obj[key]); @@ -376,15 +448,33 @@ function toExpressionValue(obj: any): ExpressionValue { } throw new FirebaseError( - `Cannot convert "${obj}" of type ${typeof obj} for Firebase Storage rules runtime` + `Cannot convert "${obj}" of type ${typeof obj} for Firebase Storage rules runtime`, ); } +async function fetchFirestoreDocument( + projectId: string, + request: RuntimeActionFirestoreDataRequest, +): Promise { + const pathname = `projects/${projectId}${request.context.path}`; + + const client = EmulatorRegistry.client(Emulators.FIRESTORE, { apiVersion: "v1", auth: true }); + try { + const doc = await client.get(pathname); + const { name, fields } = doc.body as { name: string; fields: string }; + const result = { name, fields }; + return { result, status: DataLoadStatus.OK, warnings: [], errors: [] }; + } catch (e) { + // Don't care what the error is, just return not_found + return { status: DataLoadStatus.NOT_FOUND, warnings: [], errors: [] }; + } +} + function createAuthExpressionValue(opts: RulesetVerificationOpts): ExpressionValue { if (!opts.token) { return toExpressionValue(null); } else { - const tokenPayload = jwt.decode(opts.token) as any; + const tokenPayload = jwt.decode(opts.token, { json: true }) as any; const jsonValue = { uid: tokenPayload.user_id, @@ -402,7 +492,6 @@ function createRequestExpressionValue(opts: RulesetVerificationOpts): Expression segments: opts.path .split("/") .filter((s) => s) - .slice(3) .map((simple) => ({ simple, })), @@ -410,7 +499,7 @@ function createRequestExpressionValue(opts: RulesetVerificationOpts): Expression }, time: toExpressionValue(new Date()), resource: toExpressionValue(opts.file.after ? opts.file.after : null), - auth: opts.token ? createAuthExpressionValue(opts) : { null_value: 0 }, + auth: opts.token ? createAuthExpressionValue(opts) : { null_value: null }, }; return { diff --git a/src/emulator/storage/rules/types.ts b/src/emulator/storage/rules/types.ts index a551c7343b4..c4768c1e11c 100644 --- a/src/emulator/storage/rules/types.ts +++ b/src/emulator/storage/rules/types.ts @@ -10,6 +10,12 @@ export enum RulesetOperationMethod { DELETE = "delete", } +export enum DataLoadStatus { + OK = "ok", + NOT_FOUND = "not_found", + INVALID_STATE = "invalid_state", +} + export interface Source { files: SourceFile[]; } @@ -20,8 +26,10 @@ export interface SourceFile { } export interface RuntimeActionResponse { - id: number; + id?: number; + server_request_id?: number; // Snake case comes from the server status?: string; + action?: string; message?: string; warnings: string[]; errors: string[]; @@ -33,12 +41,27 @@ export interface RuntimeActionLoadRulesetResponse extends RuntimeActionResponse }; } -export interface RuntimeActionVerifyResponse extends RuntimeActionResponse { +export type RuntimeActionVerifyResponse = + | RuntimeActionVerifyCompleteResponse + | RuntimeActionFirestoreDataRequest; + +export interface RuntimeActionVerifyCompleteResponse extends RuntimeActionResponse { result: { permit: boolean }; } +export interface RuntimeActionFirestoreDataRequest extends RuntimeActionResponse { + action: "fetch_firestore_document"; + context: { path: string }; +} + +export interface RuntimeActionFirestoreDataResponse + extends RuntimeActionResponse, + RuntimeActionBundle { + result?: unknown; +} + export interface RuntimeActionBundle { - action: string; + action?: string; } export interface RuntimeActionLoadRulesetBundle extends RuntimeActionBundle { @@ -56,6 +79,7 @@ export interface RuntimeActionVerifyBundle extends RuntimeActionBundle { service: string; path: string; method: string; + delimiter?: string; variables: { [s: string]: ExpressionValue }; }; } diff --git a/src/emulator/storage/rules/utils.ts b/src/emulator/storage/rules/utils.ts new file mode 100644 index 00000000000..69a641014cc --- /dev/null +++ b/src/emulator/storage/rules/utils.ts @@ -0,0 +1,141 @@ +import { StorageRulesetInstance } from "./runtime"; +import { RulesResourceMetadata } from "../metadata"; +import { RulesetOperationMethod } from "./types"; +import { EmulatorLogger } from "../../emulatorLogger"; +import { Emulators } from "../../types"; + +/** Variable overrides to be passed to the rules evaluator. */ +export type RulesVariableOverrides = { + before?: RulesResourceMetadata; + after?: RulesResourceMetadata; +}; + +/** Authorizes storage requests via Firebase Rules rulesets. */ +export interface FirebaseRulesValidator { + validate( + path: string, + bucketId: string, + method: RulesetOperationMethod, + variableOverrides: RulesVariableOverrides, + projectId: string, + authorization?: string, + delimiter?: string, + ): Promise; +} + +/** Authorizes storage requests via admin credentials. */ +export interface AdminCredentialValidator { + validate(authorization?: string): boolean; +} + +/** Provider for Storage security rules. */ +export type RulesetProvider = (resource: string) => StorageRulesetInstance | undefined; + +/** + * Returns a validator that pulls a Ruleset from a {@link RulesetProvider} on each run. + */ +export function getFirebaseRulesValidator( + rulesetProvider: RulesetProvider, +): FirebaseRulesValidator { + return { + validate: async ( + path: string, + bucketId: string, + method: RulesetOperationMethod, + variableOverrides: RulesVariableOverrides, + projectId: string, + authorization?: string, + delimiter?: string, + ) => { + return await isPermitted({ + ruleset: rulesetProvider(bucketId), + file: variableOverrides, + path, + method, + projectId, + authorization, + delimiter, + }); + }, + }; +} + +/** + * Returns a Firebase Rules validator returns true iff a valid OAuth (admin) credential + * is available. This validator does *not* check Firebase Rules directly. + */ +export function getAdminOnlyFirebaseRulesValidator(): FirebaseRulesValidator { + return { + /* eslint-disable @typescript-eslint/no-unused-vars */ + validate: ( + _path: string, + _bucketId: string, + _method: RulesetOperationMethod, + _variableOverrides: RulesVariableOverrides, + _authorization?: string, + delimiter?: string, + ) => { + // TODO(tonyjhuang): This should check for valid admin credentials some day. + // Unfortunately today, there's no easy way to set up the GCS SDK to pass + // "Bearer owner" along with requests so this is a placeholder. + return Promise.resolve(true); + }, + /* eslint-enable @typescript-eslint/no-unused-vars */ + }; +} + +/** + * Returns a validator for OAuth (admin) credentials. This typically takes the shape of + * "Authorization: Bearer owner" headers. + */ +export function getAdminCredentialValidator(): AdminCredentialValidator { + return { validate: isValidAdminCredentials }; +} + +/** Authorizes file access based on security rules. */ +export async function isPermitted(opts: { + ruleset?: StorageRulesetInstance; + file: { + before?: RulesResourceMetadata; + after?: RulesResourceMetadata; + }; + path: string; + method: RulesetOperationMethod; + projectId: string; + authorization?: string; + delimiter?: string; +}): Promise { + if (!opts.ruleset) { + EmulatorLogger.forEmulator(Emulators.STORAGE).log( + "WARN", + `Can not process SDK request with no loaded ruleset`, + ); + return false; + } + + // Skip auth for UI + if (isValidAdminCredentials(opts.authorization)) { + return true; + } + + const { permitted, issues } = await opts.ruleset.verify({ + method: opts.method, + path: opts.path, + file: opts.file, + projectId: opts.projectId, + token: opts.authorization ? opts.authorization.split(" ")[1] : undefined, + delimiter: opts.delimiter, + }); + + if (issues.exist()) { + issues.all.forEach((warningOrError) => { + EmulatorLogger.forEmulator(Emulators.STORAGE).log("WARN", warningOrError); + }); + } + + return !!permitted; +} + +function isValidAdminCredentials(authorization?: string) { + return ["Bearer owner", "Firebase owner"].includes(authorization ?? ""); +} diff --git a/src/emulator/storage/server.ts b/src/emulator/storage/server.ts index a02fa959b21..3462b9687f4 100644 --- a/src/emulator/storage/server.ts +++ b/src/emulator/storage/server.ts @@ -4,8 +4,10 @@ import { EmulatorLogger } from "../emulatorLogger"; import { Emulators } from "../types"; import * as bodyParser from "body-parser"; import { createCloudEndpoints } from "./apis/gcloud"; -import { StorageEmulator } from "./index"; +import { RulesConfig, StorageEmulator } from "./index"; import { createFirebaseEndpoints } from "./apis/firebase"; +import { InvalidArgumentError } from "../auth/errors"; +import { SourceFile } from "./rules/types"; /** * @param defaultProjectId @@ -13,16 +15,27 @@ import { createFirebaseEndpoints } from "./apis/firebase"; */ export function createApp( defaultProjectId: string, - emulator: StorageEmulator + emulator: StorageEmulator, ): Promise { const { storageLayer } = emulator; const app = express(); EmulatorLogger.forEmulator(Emulators.STORAGE).log( "DEBUG", - `Temp file directory for storage emulator: ${storageLayer.dirPath}` + `Temp file directory for storage emulator: ${storageLayer.dirPath}`, ); + // Return access-control-allow-private-network header if requested + // Enables accessing locahost when site is exposed via tunnel see https://github.com/firebase/firebase-tools/issues/4227 + // Aligns with https://wicg.github.io/private-network-access/#headers + // Replace with cors option if adopted, see https://github.com/expressjs/cors/issues/236 + app.use("/", (req, res, next) => { + if (req.headers["access-control-request-private-network"]) { + res.setHeader("access-control-allow-private-network", "true"); + } + next(); + }); + // Enable CORS for all APIs, all origins (reflected), and all headers (reflected). // This is similar to production behavior. Safe since all APIs are cookieless. app.use( @@ -31,8 +44,8 @@ export function createApp( exposedHeaders: [ "content-type", "x-firebase-storage-version", + "X-Goog-Upload-Size-Received", "x-goog-upload-url", - "x-goog-upload-status", "x-goog-upload-command", "x-gupload-uploadid", "x-goog-upload-header-content-length", @@ -42,7 +55,7 @@ export function createApp( "x-goog-upload-chunk-granularity", "x-goog-upload-control-url", ], - }) + }), ); app.use(bodyParser.raw({ limit: "130mb", type: "application/x-www-form-urlencoded" })); @@ -51,49 +64,94 @@ export function createApp( app.use( express.json({ type: ["application/json"], - }) + }), ); app.post("/internal/export", async (req, res) => { - const path = req.body.path; + const initiatedBy: string = req.body.initiatedBy || "unknown"; + const path: string = req.body.path; if (!path) { res.status(400).send("Export request body must include 'path'."); return; } - await storageLayer.export(path); + await storageLayer.export(path, { initiatedBy }); res.sendStatus(200); }); + /** + * Internal endpoint to overwrite current rules. Callers provide either a single set of rules to + * be applied to all resources or an array of rules/resource objects. + * + * Example payload for single set of rules: + * + * ``` + * { + * rules: { + * files: [{ name: , content: }] + * } + * } + * ``` + * + * Example payload for multiple rules/resource objects: + * + * ``` + * { + * rules: { + * files: [ + * { name: , content: , resource: }, + * ... + * ] + * } + * } + * ``` + */ app.put("/internal/setRules", async (req, res) => { - // Payload: - // { - // rules: { - // files: [{ name: content: }] - // } - // } - // TODO: Add a bucket parameter for per-bucket rules support - - const rules = req.body.rules; - if (!(rules && Array.isArray(rules.files) && rules.files.length > 0)) { - res.status(400).send("Request body must include 'rules.files' array ."); + const rulesRaw = req.body.rules; + if (!(rulesRaw && Array.isArray(rulesRaw.files) && rulesRaw.files.length > 0)) { + res.status(400).json({ + message: "Request body must include 'rules.files' array", + }); return; } - const file = rules.files[0]; - if (!(file.name && file.content)) { - res - .status(400) - .send( - "Request body must include 'rules.files' array where each member contains 'name' and 'content'." - ); - return; + const { files } = rulesRaw; + + function parseRulesFromFiles(files: Array): SourceFile | RulesConfig[] { + if (files.length === 1) { + const file = files[0]; + if (!isRulesFile(file)) { + throw new InvalidArgumentError( + "Each member of 'rules.files' array must contain 'name' and 'content'", + ); + } + return { name: file.name, content: file.content }; + } + + const rules: RulesConfig[] = []; + for (const file of files) { + if (!isRulesFile(file) || !file.resource) { + throw new InvalidArgumentError( + "Each member of 'rules.files' array must contain 'name', 'content', and 'resource'", + ); + } + rules.push({ resource: file.resource, rules: { name: file.name, content: file.content } }); + } + return rules; } - const name = file.name; - const content = file.content; - const issues = await emulator.loadRuleset({ files: [{ name, content }] }); + let rules: SourceFile | RulesConfig[]; + try { + rules = parseRulesFromFiles(files); + } catch (err) { + if (err instanceof InvalidArgumentError) { + res.status(400).json({ message: err.message }); + return; + } + throw err; + } + const issues = await emulator.replaceRules(rules); if (issues.errors.length > 0) { res.status(400).json({ message: "There was an error updating rules, see logs for more details", @@ -107,7 +165,7 @@ export function createApp( }); app.post("/internal/reset", (req, res) => { - storageLayer.reset(); + emulator.reset(); res.sendStatus(200); }); @@ -116,3 +174,15 @@ export function createApp( return Promise.resolve(app); } + +interface RulesFile { + name: string; + content: string; + resource?: string; +} + +function isRulesFile(file: unknown): file is RulesFile { + return ( + typeof (file as RulesFile).name === "string" && typeof (file as RulesFile).content === "string" + ); +} diff --git a/src/emulator/storage/upload.spec.ts b/src/emulator/storage/upload.spec.ts new file mode 100644 index 00000000000..75810fe9bb1 --- /dev/null +++ b/src/emulator/storage/upload.spec.ts @@ -0,0 +1,27 @@ +/* + it("should store file in memory when upload is finalized", () => { + const storageLayer = getStorageLayer(ALWAYS_TRUE_RULES_VALIDATOR); + const bytesToWrite = "Hello, World!"; + + + const upload = storageLayer.startUpload("bucket", "object", "mime/type", { + contentType: "mime/type", + }); + storageLayer.uploadBytes(upload.uploadId, Buffer.from(bytesToWrite)); + storageLayer.finalizeUpload(upload); + + expect(storageLayer.getBytes("bucket", "object")?.includes(bytesToWrite)); + expect(storageLayer.getMetadata("bucket", "object")?.size).equals(bytesToWrite.length); + }); + + it("should delete file from persistence layer when upload is cancelled", () => { + const storageLayer = getStorageLayer(ALWAYS_TRUE_RULES_VALIDATOR); + + const upload = storageLayer.startUpload("bucket", "object", "mime/type", { + contentType: "mime/type", + }); + storageLayer.uploadBytes(upload.uploadId, Buffer.alloc(0)); + storageLayer.cancelUpload(upload); + + expect(storageLayer.getMetadata("bucket", "object")).to.equal(undefined); + });*/ diff --git a/src/emulator/storage/upload.ts b/src/emulator/storage/upload.ts new file mode 100644 index 00000000000..991a8b9c182 --- /dev/null +++ b/src/emulator/storage/upload.ts @@ -0,0 +1,250 @@ +import { Persistence } from "./persistence"; +import { IncomingMetadata } from "./metadata"; +import { v4 as uuidV4 } from "uuid"; +import { NotFoundError } from "./errors"; + +/** A file upload. */ +export type Upload = { + id: string; + bucketId: string; + objectId: string; + type: UploadType; + // Path to where the file is stored on disk. May contain incomplete data if + // status !== FINISHED. + path: string; + status: UploadStatus; + metadata?: IncomingMetadata; + size: number; + authorization?: string; + prevResponseCode?: number; +}; + +export enum UploadType { + MEDIA, + MULTIPART, + RESUMABLE, +} + +/** The status of an upload. Multipart uploads can only ever be FINISHED. */ +export enum UploadStatus { + ACTIVE = "active", + CANCELLED = "cancelled", + FINISHED = "final", +} + +/** Request object for {@link UploadService#mediaUpload}. */ +export type MediaUploadRequest = { + bucketId: string; + objectId: string; + dataRaw: Buffer; + authorization?: string; +}; + +/** Request object for {@link UploadService#multipartUpload}. */ +export type MultipartUploadRequest = { + bucketId: string; + objectId: string; + metadata: object; + dataRaw: Buffer; + authorization?: string; +}; + +/** Request object for {@link UploadService#startResumableUpload}. */ +export type StartResumableUploadRequest = { + bucketId: string; + objectId: string; + metadata: object; + authorization?: string; +}; + +type OneShotUploadRequest = { + bucketId: string; + objectId: string; + uploadType: UploadType; + dataRaw: Buffer; + metadata?: any; + authorization?: string; +}; + +/** Error that signals a resumable upload that's expected to be active is not. */ +export class UploadNotActiveError extends Error {} + +/** Error that signals a resumable upload that shouldn't be finalized is. */ +export class UploadPreviouslyFinalizedError extends Error {} + +/** Error that signals a resumable upload is not cancellable. */ +export class NotCancellableError extends Error {} + +/** + * Service that handles byte transfer and maintains state for file uploads. + * + * New file uploads will be persisted to a temp staging directory which will not + * survive across emulator restarts. Clients are expected to move staged files + * to a more permanent location. + */ +export class UploadService { + private _uploads!: Map; + constructor(private _persistence: Persistence) { + this.reset(); + } + + /** Resets the state of the UploadService. */ + public reset(): void { + this._uploads = new Map(); + } + + /** Handles a media (data-only) file upload. */ + public mediaUpload(request: MediaUploadRequest): Upload { + const upload = this.startOneShotUpload({ + bucketId: request.bucketId, + objectId: request.objectId, + uploadType: UploadType.MEDIA, + dataRaw: request.dataRaw, + authorization: request.authorization, + }); + this._persistence.deleteFile(upload.path, /* failSilently = */ true); + this._persistence.appendBytes(upload.path, request.dataRaw); + return upload; + } + + /** + * Handles a multipart file upload which is expected to have the entirety of + * the file's contents in a single request. + */ + public multipartUpload(request: MultipartUploadRequest): Upload { + const upload = this.startOneShotUpload({ + bucketId: request.bucketId, + objectId: request.objectId, + uploadType: UploadType.MULTIPART, + dataRaw: request.dataRaw, + metadata: request.metadata, + authorization: request.authorization, + }); + this._persistence.deleteFile(upload.path, /* failSilently = */ true); + this._persistence.appendBytes(upload.path, request.dataRaw); + return upload; + } + + private startOneShotUpload(request: OneShotUploadRequest): Upload { + const id = uuidV4(); + const upload: Upload = { + id, + bucketId: request.bucketId, + objectId: request.objectId, + type: request.uploadType, + path: this.getStagingFileName(id, request.bucketId, request.objectId), + status: UploadStatus.FINISHED, + metadata: request.metadata, + size: request.dataRaw.byteLength, + authorization: request.authorization, + }; + this._uploads.set(upload.id, upload); + + return upload; + } + + /** + * Initializes a new ResumableUpload. + */ + public startResumableUpload(request: StartResumableUploadRequest): Upload { + const id = uuidV4(); + const upload: Upload = { + id: id, + bucketId: request.bucketId, + objectId: request.objectId, + type: UploadType.RESUMABLE, + path: this.getStagingFileName(id, request.bucketId, request.objectId), + status: UploadStatus.ACTIVE, + metadata: request.metadata, + size: 0, + authorization: request.authorization, + }; + this._uploads.set(upload.id, upload); + this._persistence.deleteFile(upload.path, /* failSilently = */ true); + + // create empty file to append to later + this._persistence.appendBytes(upload.path, Buffer.alloc(0)); + return upload; + } + + /** + * Appends bytes to an existing resumable upload. + * @throws {NotFoundError} if the resumable upload does not exist. + * @throws {NotActiveUploadError} if the resumable upload is not in the ACTIVE state. + */ + public continueResumableUpload(uploadId: string, dataRaw: Buffer): Upload { + const upload = this.getResumableUpload(uploadId); + if (upload.status !== UploadStatus.ACTIVE) { + throw new UploadNotActiveError(); + } + this._persistence.appendBytes(upload.path, dataRaw); + upload.size += dataRaw.byteLength; + return upload; + } + + /** + * Queries for an existing resumable upload. + * @throws {NotFoundError} if the resumable upload does not exist. + */ + public getResumableUpload(uploadId: string): Upload { + const upload = this._uploads.get(uploadId); + if (!upload || upload.type !== UploadType.RESUMABLE) { + throw new NotFoundError(); + } + return upload; + } + + /** + * Cancels a resumable upload. + * @throws {NotFoundError} if the resumable upload does not exist. + * @throws {NotCancellableError} if the resumable upload can not be cancelled. + */ + public cancelResumableUpload(uploadId: string): Upload { + const upload = this.getResumableUpload(uploadId); + if (upload.status === UploadStatus.FINISHED) { + throw new NotCancellableError(); + } + upload.status = UploadStatus.CANCELLED; + return upload; + } + + /** + * Marks a ResumableUpload as finalized. + * @throws {NotFoundError} if the resumable upload does not exist. + * @throws {UploadNotActiveError} if the resumable upload is not ACTIVE. + * @throws {UploadPreviouslyFinalizedError} if the resumable upload has already been finalized. + */ + public finalizeResumableUpload(uploadId: string): Upload { + const upload = this.getResumableUpload(uploadId); + if (upload.status === UploadStatus.FINISHED) { + throw new UploadPreviouslyFinalizedError(); + } + if (upload.status === UploadStatus.CANCELLED) { + throw new UploadNotActiveError(); + } + upload.status = UploadStatus.FINISHED; + return upload; + } + + /** + * Sets previous response code. + */ + public setResponseCode(uploadId: string, code: number): void { + const upload = this._uploads.get(uploadId); + if (upload) { + upload.prevResponseCode = code; + } + } + + /** + * Gets previous response code. + * In the case the uploadId doesn't exist (after importing) return 200 + */ + public getPreviousResponseCode(uploadId: string): number { + return this._uploads.get(uploadId)?.prevResponseCode || 200; + } + + private getStagingFileName(uploadId: string, bucketId: string, objectId: string): string { + return encodeURIComponent(`${uploadId}_b_${bucketId}_o_${objectId}`); + } +} diff --git a/src/emulator/testing/fakeEmulator.ts b/src/emulator/testing/fakeEmulator.ts new file mode 100644 index 00000000000..81e9326f07f --- /dev/null +++ b/src/emulator/testing/fakeEmulator.ts @@ -0,0 +1,28 @@ +import { Emulators, ListenSpec } from "../types"; +import { ExpressBasedEmulator } from "../ExpressBasedEmulator"; +import { resolveHostAndAssignPorts } from "../portUtils"; + +/** + * A thing that acts like an emulator by just occupying a port. + */ +export class FakeEmulator extends ExpressBasedEmulator { + constructor( + public name: Emulators, + listen: ListenSpec[], + ) { + super({ listen, noBodyParser: true, noCors: true }); + } + getName(): Emulators { + return this.name; + } + + static async create(name: Emulators, host = "127.0.0.1"): Promise { + const listen = await resolveHostAndAssignPorts({ + [name]: { + host, + port: 4000, + }, + }); + return new FakeEmulator(name, listen[name]); + } +} diff --git a/src/test/emulators/fixtures.ts b/src/emulator/testing/fixtures.ts similarity index 92% rename from src/test/emulators/fixtures.ts rename to src/emulator/testing/fixtures.ts index c1ed615257f..e3aa361f5c1 100644 --- a/src/test/emulators/fixtures.ts +++ b/src/emulator/testing/fixtures.ts @@ -1,22 +1,18 @@ -import { findModuleRoot, FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; +import * as fs from "fs"; +import * as path from "path"; +import { tmpdir } from "os"; +import { findModuleRoot, FunctionsRuntimeBundle } from "../functionsEmulatorShared"; export const TIMEOUT_LONG = 10000; export const TIMEOUT_MED = 5000; +export function createTmpDir(dirName: string) { + return fs.mkdtempSync(path.join(tmpdir(), dirName)); +} + export const MODULE_ROOT = findModuleRoot("firebase-tools", __dirname); export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = { onCreate: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { value: { @@ -41,22 +37,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "region-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onWrite: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { value: { @@ -81,22 +63,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "region-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onDelete: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { oldValue: { @@ -121,22 +89,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "region-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onUpdate: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, proto: { data: { oldValue: { @@ -173,25 +127,9 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = timestamp: "2019-05-15T16:21:15.148831Z", }, }, - triggerId: "region-function_id", - targetName: "function_id", - projectId: "fake-project-id", }, onRequest: { - adminSdkConfig: { - databaseURL: "https://fake-project-id-default-rtdb.firebaseio.com", - storageBucket: "fake-project-id.appspot.com", - }, - emulators: { - firestore: { - host: "localhost", - port: 8080, - }, - }, - cwd: MODULE_ROOT, - triggerId: "region-function_id", - targetName: "function_id", - projectId: "fake-project-id", + proto: {}, }, }; @@ -294,36 +232,3 @@ service firebase.storage { `, }, }; - -/* -service firebase.storage { - match /b/{bucket}/o { - match /authIsNotNull { - allow read, write: if request.auth != null; - } - - match /authUidMatchesPath/{uid} { - allow read: if request.auth.uid == uid - } - - match /imageSourceSizeUnder5MbAndContentTypeIsImage { - // Only allow uploads of any image file that's less than 5MB - allow write: if request.resource.size < 5 * 1024 * 1024 - && request.resource.contentType.matches('image/.*'); - } - - match /customMetadataAndcustomTokenField { - allow read: if resource.metadata.owner == request.auth.token.groupId; - allow write: if request.auth.token.groupId == groupId; - } - - function signedInOrHasVisibility(visibility) { - return request.auth.uid != null || resource.metadata.visibility == visibility; - } - match /signInWithFuntion/{visiblityParams} { - allow read, write: if signedInOrHasVisibility(visiblityParams); - } - - } -} - */ diff --git a/src/emulator/types.ts b/src/emulator/types.ts index 458c0005085..c59884fdc99 100644 --- a/src/emulator/types.ts +++ b/src/emulator/types.ts @@ -1,6 +1,5 @@ import { ChildProcess } from "child_process"; import { EventEmitter } from "events"; -import { previews } from "../previews"; export enum Emulators { AUTH = "auth", @@ -13,6 +12,9 @@ export enum Emulators { UI = "ui", LOGGING = "logging", STORAGE = "storage", + EXTENSIONS = "extensions", + EVENTARC = "eventarc", + DATACONNECT = "dataconnect", } export type DownloadableEmulators = @@ -20,13 +22,15 @@ export type DownloadableEmulators = | Emulators.DATABASE | Emulators.PUBSUB | Emulators.UI - | Emulators.STORAGE; + | Emulators.STORAGE + | Emulators.DATACONNECT; export const DOWNLOADABLE_EMULATORS = [ Emulators.FIRESTORE, Emulators.DATABASE, Emulators.PUBSUB, Emulators.UI, Emulators.STORAGE, + Emulators.DATACONNECT, ]; export type ImportExportEmulators = Emulators.FIRESTORE | Emulators.DATABASE | Emulators.AUTH; @@ -45,13 +49,16 @@ export const ALL_SERVICE_EMULATORS = [ Emulators.HOSTING, Emulators.PUBSUB, Emulators.STORAGE, -].filter((v) => v) as Emulators[]; + Emulators.EVENTARC, + Emulators.DATACONNECT, +].filter((v) => v); export const EMULATORS_SUPPORTED_BY_FUNCTIONS = [ Emulators.FIRESTORE, Emulators.DATABASE, Emulators.PUBSUB, Emulators.STORAGE, + Emulators.EVENTARC, ]; export const EMULATORS_SUPPORTED_BY_UI = [ @@ -60,6 +67,7 @@ export const EMULATORS_SUPPORTED_BY_UI = [ Emulators.FIRESTORE, Emulators.FUNCTIONS, Emulators.STORAGE, + Emulators.EXTENSIONS, ]; export const EMULATORS_SUPPORTED_BY_USE_EMULATOR = [ @@ -67,6 +75,7 @@ export const EMULATORS_SUPPORTED_BY_USE_EMULATOR = [ Emulators.DATABASE, Emulators.FIRESTORE, Emulators.FUNCTIONS, + Emulators.STORAGE, ]; // TODO: Is there a way we can just allow iteration over the enum? @@ -74,6 +83,7 @@ export const ALL_EMULATORS = [ Emulators.HUB, Emulators.UI, Emulators.LOGGING, + Emulators.EXTENSIONS, ...ALL_SERVICE_EMULATORS, ]; @@ -125,9 +135,18 @@ export interface EmulatorInstance { export interface EmulatorInfo { name: Emulators; + pid?: number; + reservedPorts?: number[]; + + // All addresses that an emulator listens on. + listen?: ListenSpec[]; + + // The primary IP address that the emulator listens on. host: string; port: number; - pid?: number; + + // How long to wait for the emulator to start before erroring out. + timeout?: number; } export interface DownloadableEmulatorCommand { @@ -135,6 +154,7 @@ export interface DownloadableEmulatorCommand { args: string[]; optionalArgs: string[]; joinArgs: boolean; + shell: boolean; } export interface EmulatorDownloadOptions { @@ -145,6 +165,13 @@ export interface EmulatorDownloadOptions { namePrefix: string; skipChecksumAndSize?: boolean; skipCache?: boolean; + auth?: boolean; +} + +export interface EmulatorUpdateDetails { + version: string; + expectedSize: number; + expectedChecksum: string; } export interface EmulatorDownloadDetails { @@ -163,6 +190,9 @@ export interface EmulatorDownloadDetails { // If specified, a path where the runnable binary can be found after downloading and // unzipping. Otherwise downloadPath will be used. binaryPath?: string; + + // If true, never try to download this emualtor. Set when developing with local versions of an emulator. + localOnly?: boolean; } export interface DownloadableEmulatorDetails { @@ -171,9 +201,10 @@ export interface DownloadableEmulatorDetails { stdout: any | null; } -export interface Address { - host: string; +export interface ListenSpec { + address: string; port: number; + family: "IPv4" | "IPv6"; } export enum FunctionsExecutionMode { @@ -208,9 +239,9 @@ export class EmulatorLog { emitter: EventEmitter, level: string, type: string, - filter?: (el: EmulatorLog) => boolean + filter?: (el: EmulatorLog) => boolean, ): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const listener = (el: EmulatorLog) => { const levelTypeMatch = el.level === level && el.type === type; let filterMatch = true; @@ -232,7 +263,7 @@ export class EmulatorLog { let isNotJSON = false; try { parsedLog = JSON.parse(json); - } catch (err) { + } catch (err: any) { isNotJSON = true; } @@ -256,7 +287,7 @@ export class EmulatorLog { parsedLog.type, parsedLog.text, parsedLog.data, - parsedLog.timestamp + parsedLog.timestamp, ); } @@ -268,7 +299,7 @@ export class EmulatorLog { public type: string, public text: string, public data?: any, - public timestamp?: string + public timestamp?: string, ) { this.timestamp = this.timestamp || new Date().toISOString(); this.data = this.data || {}; @@ -318,7 +349,7 @@ export class EmulatorLog { }); } else { process.stderr.write( - "subprocess.send() is undefined, cannot communicate with Functions Runtime." + "subprocess.send() is undefined, cannot communicate with Functions Runtime.", ); } } @@ -333,7 +364,7 @@ export class EmulatorLog { type: this.type, }, undefined, - pretty ? 2 : 0 + pretty ? 2 : 0, ); } } diff --git a/src/emulator/ui.ts b/src/emulator/ui.ts index da350c834d6..387f4be9765 100644 --- a/src/emulator/ui.ts +++ b/src/emulator/ui.ts @@ -1,12 +1,14 @@ -import { EmulatorInstance, EmulatorInfo, Emulators } from "./types"; +import { EmulatorInstance, EmulatorInfo, Emulators, ListenSpec } from "./types"; import * as downloadableEmulators from "./downloadableEmulators"; import { EmulatorRegistry } from "./registry"; import { FirebaseError } from "../error"; import { Constants } from "./constants"; +import { emulatorSession } from "../track"; +import { ExpressBasedEmulator } from "./ExpressBasedEmulator"; +import { ALL_EXPERIMENTS, ExperimentName, isEnabled } from "../experiments"; export interface EmulatorUIOptions { - port: number; - host: string; + listen: ListenSpec[]; projectId: string; auto_download?: boolean; } @@ -18,20 +20,28 @@ export class EmulatorUI implements EmulatorInstance { if (!EmulatorRegistry.isRunning(Emulators.HUB)) { throw new FirebaseError( `Cannot start ${Constants.description(Emulators.UI)} without ${Constants.description( - Emulators.HUB - )}!` + Emulators.HUB, + )}!`, ); } - const hubInfo = EmulatorRegistry.get(Emulators.HUB)!.getInfo(); - const { auto_download, host, port, projectId } = this.args; - const env: NodeJS.ProcessEnv = { - HOST: host.toString(), - PORT: port.toString(), + const { auto_download: autoDownload, projectId } = this.args; + const env: Partial = { + LISTEN: JSON.stringify(ExpressBasedEmulator.listenOptionsFromSpecs(this.args.listen)), GCLOUD_PROJECT: projectId, - [Constants.FIREBASE_EMULATOR_HUB]: EmulatorRegistry.getInfoHostString(hubInfo), + [Constants.FIREBASE_EMULATOR_HUB]: EmulatorRegistry.url(Emulators.HUB).host, }; - return downloadableEmulators.start(Emulators.UI, { auto_download }, env); + const session = emulatorSession(); + if (session) { + env[Constants.FIREBASE_GA_SESSION] = JSON.stringify(session); + } + + const enabledExperiments = (Object.keys(ALL_EXPERIMENTS) as Array).filter( + (experimentName) => isEnabled(experimentName), + ); + env[Constants.FIREBASE_ENABLED_EXPERIMENTS] = JSON.stringify(enabledExperiments); + + return downloadableEmulators.start(Emulators.UI, { auto_download: autoDownload }, env); } connect(): Promise { @@ -45,8 +55,8 @@ export class EmulatorUI implements EmulatorInstance { getInfo(): EmulatorInfo { return { name: this.getName(), - host: this.args.host, - port: this.args.port, + host: this.args.listen[0].address, + port: this.args.listen[0].port, pid: downloadableEmulators.getPID(Emulators.UI), }; } diff --git a/src/test/emulators/workQueue.spec.ts b/src/emulator/workQueue.spec.ts similarity index 95% rename from src/test/emulators/workQueue.spec.ts rename to src/emulator/workQueue.spec.ts index dda510f275f..df2ad4dbbb1 100644 --- a/src/test/emulators/workQueue.spec.ts +++ b/src/emulator/workQueue.spec.ts @@ -1,14 +1,14 @@ import { expect } from "chai"; -import { WorkQueue } from "../../emulator/workQueue"; -import { FunctionsExecutionMode } from "../../emulator/types"; +import { WorkQueue } from "./workQueue"; +import { FunctionsExecutionMode } from "./types"; function resolveIn(ms: number) { if (ms === 0) { return Promise.resolve(); } - return new Promise((res, rej) => { + return new Promise((res) => { setTimeout(res, ms); }); } diff --git a/src/emulator/workQueue.ts b/src/emulator/workQueue.ts index c32fdaf67ec..dc421bf10af 100644 --- a/src/emulator/workQueue.ts +++ b/src/emulator/workQueue.ts @@ -4,7 +4,10 @@ import { FirebaseError } from "../error"; import { EmulatorLogger } from "./emulatorLogger"; import { Emulators, FunctionsExecutionMode } from "./types"; -type Work = () => Promise; +export type Work = { + type?: string; + (): Promise; +}; /** * Queue for doing async work that can either run all work concurrently @@ -17,13 +20,13 @@ type Work = () => Promise; export class WorkQueue { private static MAX_PARALLEL_ENV = "FUNCTIONS_EMULATOR_PARALLEL"; private static DEFAULT_MAX_PARALLEL = Number.parseInt( - utils.envOverride(WorkQueue.MAX_PARALLEL_ENV, "50") + utils.envOverride(WorkQueue.MAX_PARALLEL_ENV, "50"), ); private logger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); private queue: Array = []; - private workRunningCount: number = 0; + private running: Array = []; private notifyQueue: () => void = () => { // Noop by default, will be set by .start() when queue is empty. }; @@ -34,13 +37,13 @@ export class WorkQueue { constructor( private mode: FunctionsExecutionMode = FunctionsExecutionMode.AUTO, - private maxParallelWork: number = WorkQueue.DEFAULT_MAX_PARALLEL + private maxParallelWork: number = WorkQueue.DEFAULT_MAX_PARALLEL, ) { if (maxParallelWork < 1) { throw new FirebaseError( `Cannot run Functions emulator with less than 1 parallel worker (${ WorkQueue.MAX_PARALLEL_ENV - }=${process.env[WorkQueue.MAX_PARALLEL_ENV]})` + }=${process.env[WorkQueue.MAX_PARALLEL_ENV]})`, ); } } @@ -68,19 +71,19 @@ export class WorkQueue { while (!this.stopped) { // If the queue is empty, wait until something is added. if (!this.queue.length) { - await new Promise((res) => { + await new Promise((res) => { this.notifyQueue = res; }); } // If we have too many jobs out, wait until something finishes. - if (this.workRunningCount >= this.maxParallelWork) { + if (this.running.length >= this.maxParallelWork) { this.logger.logLabeled( "DEBUG", "work-queue", - `waiting for work to finish (running=${this.workRunningCount})` + `waiting for work to finish (running=${this.running})`, ); - await new Promise((res) => { + await new Promise((res) => { this.notifyWorkFinish = res; }); } @@ -99,13 +102,13 @@ export class WorkQueue { this.stopped = true; } - async flush(timeoutMs: number = 60000) { + async flush(timeoutMs = 60000) { if (!this.isWorking()) { return; } this.logger.logLabeled("BULLET", "functions", "Waiting for all functions to finish..."); - return new Promise((res, rej) => { + return new Promise((res, rej) => { const delta = 100; let elapsed = 0; @@ -125,8 +128,10 @@ export class WorkQueue { getState() { return { + queuedWork: this.queue.map((work) => work.type), queueLength: this.queue.length, - workRunningCount: this.workRunningCount, + runningWork: this.running, + workRunningCount: this.running.length, }; } @@ -138,15 +143,18 @@ export class WorkQueue { private async runNext() { const next = this.queue.shift(); if (next) { - this.workRunningCount++; + this.running.push(next.type || "anonymous"); this.logState(); try { await next(); - } catch (e) { + } catch (e: any) { this.logger.log("DEBUG", e); } finally { - this.workRunningCount--; + const index = this.running.indexOf(next.type || "anonymous"); + if (index !== -1) { + this.running.splice(index, 1); + } this.notifyWorkFinish(); this.logState(); } diff --git a/src/ensureApiEnabled.spec.ts b/src/ensureApiEnabled.spec.ts new file mode 100644 index 00000000000..1d7d82149e3 --- /dev/null +++ b/src/ensureApiEnabled.spec.ts @@ -0,0 +1,130 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import { check, ensure, POLL_SETTINGS } from "./ensureApiEnabled"; + +const FAKE_PROJECT_ID = "my_project"; +const FAKE_API = "myapi.googleapis.com"; + +describe("ensureApiEnabled", () => { + describe("check", () => { + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + for (const prefix of ["", "https://", "http://"]) { + it("should call the API to check if it's enabled", async () => { + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .reply(200, { state: "ENABLED" }); + + await check(FAKE_PROJECT_ID, prefix + FAKE_API, "", true); + + expect(nock.isDone()).to.be.true; + }); + + it("should return the value from the API", async () => { + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "ENABLED" }); + + await expect(check(FAKE_PROJECT_ID, prefix + FAKE_API, "", true)).to.eventually.be.true; + + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "DISABLED" }); + + await expect(check(FAKE_PROJECT_ID, prefix + FAKE_API, "", true)).to.eventually.be.false; + }); + } + }); + + describe("ensure", () => { + const originalPollInterval = POLL_SETTINGS.pollInterval; + const originalPollsBeforeRetry = POLL_SETTINGS.pollsBeforeRetry; + beforeEach(() => { + nock.disableNetConnect(); + POLL_SETTINGS.pollInterval = 0; + POLL_SETTINGS.pollsBeforeRetry = 0; // Zero means "one check". + }); + + afterEach(() => { + nock.enableNetConnect(); + POLL_SETTINGS.pollInterval = originalPollInterval; + POLL_SETTINGS.pollsBeforeRetry = originalPollsBeforeRetry; + }); + + for (const prefix of ["", "https://", "http://"]) { + it("should verify that the API is enabled, and stop if it is", async () => { + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "ENABLED" }); + + await expect(ensure(FAKE_PROJECT_ID, prefix + FAKE_API, "", true)).to.not.be.rejected; + }); + + it("should attempt to enable the API if it is not enabled", async () => { + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "DISABLED" }); + + nock("https://serviceusage.googleapis.com") + .post(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}:enable`, (body) => !body) + .once() + .reply(200); + + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "ENABLED" }); + + await expect(ensure(FAKE_PROJECT_ID, prefix + FAKE_API, "", true)).to.not.be.rejected; + + expect(nock.isDone()).to.be.true; + }); + + it("should retry enabling the API if it does not enable in time", async () => { + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "DISABLED" }); + + nock("https://serviceusage.googleapis.com") + .post(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}:enable`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .twice() + .reply(200); + + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "DISABLED" }); + + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) + .matchHeader("x-goog-quota-user", `projects/${FAKE_PROJECT_ID}`) + .once() + .reply(200, { state: "ENABLED" }); + + await expect(ensure(FAKE_PROJECT_ID, prefix + FAKE_API, "", true)).to.not.be.rejected; + + expect(nock.isDone()).to.be.true; + }); + } + }); +}); diff --git a/src/ensureApiEnabled.ts b/src/ensureApiEnabled.ts index cf328079ad8..781f10d5957 100644 --- a/src/ensureApiEnabled.ts +++ b/src/ensureApiEnabled.ts @@ -1,6 +1,6 @@ -import { bold } from "cli-color"; +import { bold } from "colorette"; -import * as track from "./track"; +import { trackGA4 } from "./track"; import { serviceUsageOrigin } from "./api"; import { Client } from "./apiv2"; import * as utils from "./utils"; @@ -12,24 +12,28 @@ export const POLL_SETTINGS = { }; const apiClient = new Client({ - urlPrefix: serviceUsageOrigin, + urlPrefix: serviceUsageOrigin(), apiVersion: "v1", }); /** * Check if the specified API is enabled. * @param projectId The project on which to check enablement. - * @param apiName The name of the API e.g. `someapi.googleapis.com`. + * @param apiUri The name of the API e.g. `someapi.googleapis.com`. * @param prefix The logging prefix to use when printing messages about enablement. * @param silent Whether or not to print log messages. */ export async function check( projectId: string, - apiName: string, + apiUri: string, prefix: string, - silent = false + silent = false, ): Promise { - const res = await apiClient.get<{ state: string }>(`/projects/${projectId}/services/${apiName}`); + const apiName = apiUri.startsWith("http") ? new URL(apiUri).hostname : apiUri; + const res = await apiClient.get<{ state: string }>(`/projects/${projectId}/services/${apiName}`, { + headers: { "x-goog-quota-user": `projects/${projectId}` }, + skipLog: { resBody: true }, + }); const isEnabled = res.body.state === "ENABLED"; if (isEnabled && !silent) { utils.logLabeledSuccess(prefix, `required API ${bold(apiName)} is enabled`); @@ -37,6 +41,10 @@ export async function check( return isEnabled; } +function isPermissionError(e: { context?: { body?: { error?: { status?: string } } } }): boolean { + return e.context?.body?.error?.status === "PERMISSION_DENIED"; +} + /** * Attempt to enable an API on the specified project (just once). * @@ -48,18 +56,47 @@ export async function check( */ async function enable(projectId: string, apiName: string): Promise { try { - await apiClient.post(`/projects/${projectId}/services/${apiName}:enable`); - } catch (err) { + await apiClient.post( + `/projects/${projectId}/services/${apiName}:enable`, + undefined, + { + headers: { "x-goog-quota-user": `projects/${projectId}` }, + skipLog: { resBody: true }, + }, + ); + } catch (err: any) { if (isBillingError(err)) { throw new FirebaseError(`Your project ${bold( - projectId + projectId, )} must be on the Blaze (pay-as-you-go) plan to complete this command. Required API ${bold( - apiName + apiName, )} can't be enabled until the upgrade is complete. To upgrade, visit the following URL: https://console.firebase.google.com/project/${projectId}/usage/details`); + } else if (isPermissionError(err)) { + const apiPermissionDeniedRegex = new RegExp( + /Permission denied to enable service \[([.a-zA-Z]+)\]/, + ); + // Recognize permission denied errors on APIs and provide users the + // GCP console link to easily enable the API. + const permissionsError = apiPermissionDeniedRegex.exec((err as Error).message); + if (permissionsError && permissionsError[1]) { + const serviceUrl = permissionsError[1]; + // Expand the error message instead of creating a new error so that + // all the other error properties (status, context, etc) are passed + // downstream to anything that uses them. + (err as Error).message = `Permissions denied enabling ${serviceUrl}. + Please ask a project owner to visit the following URL to enable this service: + + https://console.cloud.google.com/apis/library/${serviceUrl}?project=${projectId}`; + throw err; + } else { + // Regex failed somehow - show the raw permissions error. + throw err; + } + } else { + throw err; } - throw err; } } @@ -69,7 +106,7 @@ async function pollCheckEnabled( prefix: string, silent: boolean, enablementRetries: number, - pollRetries = 0 + pollRetries = 0, ): Promise { if (pollRetries > POLL_SETTINGS.pollsBeforeRetry) { // eslint-disable-next-line @typescript-eslint/no-use-before-define @@ -81,7 +118,9 @@ async function pollCheckEnabled( }); const isEnabled = await check(projectId, apiName, prefix, silent); if (isEnabled) { - void track("api_enabled", apiName); + void trackGA4("api_enabled", { + api_name: apiName, + }); return; } if (!silent) { @@ -95,11 +134,11 @@ async function enableApiWithRetries( apiName: string, prefix: string, silent: boolean, - enablementRetries = 0 + enablementRetries = 0, ): Promise { if (enablementRetries > 1) { throw new FirebaseError( - `Timed out waiting for API ${bold(apiName)} to enable. Please try again in a few minutes.` + `Timed out waiting for API ${bold(apiName)} to enable. Please try again in a few minutes.`, ); } await enable(projectId, apiName); @@ -110,25 +149,38 @@ async function enableApiWithRetries( * Check if an API is enabled on a project, try to enable it if not with polling and retries. * * @param projectId The project on which to check enablement. - * @param apiName The name of the API e.g. `someapi.googleapis.com`. + * @param apiUri The name of the API e.g. `someapi.googleapis.com`. * @param prefix The logging prefix to use when printing messages about enablement. * @param silent Whether or not to print log messages. */ export async function ensure( projectId: string, - apiName: string, + apiUri: string, prefix: string, - silent = false + silent = false, ): Promise { + const hostname = apiUri.startsWith("http") ? new URL(apiUri).hostname : apiUri; if (!silent) { - utils.logLabeledBullet(prefix, `ensuring required API ${bold(apiName)} is enabled...`); + utils.logLabeledBullet(prefix, `ensuring required API ${bold(hostname)} is enabled...`); } - const isEnabled = await check(projectId, apiName, prefix, silent); + const isEnabled = await check(projectId, hostname, prefix, silent); if (isEnabled) { return; } if (!silent) { - utils.logLabeledWarning(prefix, `missing required API ${bold(apiName)}. Enabling now...`); + utils.logLabeledWarning(prefix, `missing required API ${bold(hostname)}. Enabling now...`); } - return enableApiWithRetries(projectId, apiName, prefix, silent); + return enableApiWithRetries(projectId, hostname, prefix, silent); +} + +/** + * Returns a link to enable an API on a project in Cloud console. This can be used instead of ensure + * in contexts where automatically enabling APIs is not desirable (ie emulator commands). + * + * @param projectId The project to generate an API enablement link for + * @param apiName The name of the API e.g. `someapi.googleapis.com`. + * @return A link to Cloud console to enable the API + */ +export function enableApiURI(projectId: string, apiName: string): string { + return `https://console.cloud.google.com/apis/library/${apiName}?project=${projectId}`; } diff --git a/src/ensureCloudResourceLocation.ts b/src/ensureCloudResourceLocation.ts index 4262a56bd43..ea73aed7156 100644 --- a/src/ensureCloudResourceLocation.ts +++ b/src/ensureCloudResourceLocation.ts @@ -14,7 +14,7 @@ export function ensureLocationSet(location: string, feature: string): void { throw new FirebaseError( `Cloud resource location is not set for this project but the operation ` + `you are attempting to perform in ${feature} requires it. ` + - `Please see this documentation for more details: https://firebase.google.com/docs/projects/locations` + `Please see this documentation for more details: https://firebase.google.com/docs/projects/locations`, ); } } diff --git a/src/test/error.spec.ts b/src/error.spec.ts similarity index 96% rename from src/test/error.spec.ts rename to src/error.spec.ts index 996f861f781..acf2386e194 100644 --- a/src/test/error.spec.ts +++ b/src/error.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { FirebaseError } from "../error"; +import { FirebaseError } from "./error"; describe("error", () => { describe("FirebaseError", () => { diff --git a/src/error.ts b/src/error.ts index 27faf34fee8..e965fa51408 100644 --- a/src/error.ts +++ b/src/error.ts @@ -53,7 +53,7 @@ export function isBillingError(e: { return !!e.context?.body?.error?.details?.find((d) => { return ( d.violations?.find((v) => v.type === "serviceusage/billing-enabled") || - d.reason == "UREQ_PROJECT_BILLING_NOT_FOUND" + d.reason === "UREQ_PROJECT_BILLING_NOT_FOUND" ); }); } diff --git a/src/errorOut.ts b/src/errorOut.ts index 48dc1150810..d361768240e 100644 --- a/src/errorOut.ts +++ b/src/errorOut.ts @@ -1,4 +1,4 @@ -import logError = require("./logError"); +import { logError } from "./logError"; import { FirebaseError } from "./error"; /** diff --git a/src/experiments.spec.ts b/src/experiments.spec.ts new file mode 100644 index 00000000000..55f84c8a6b1 --- /dev/null +++ b/src/experiments.spec.ts @@ -0,0 +1,30 @@ +import { expect } from "chai"; +import { enableExperimentsFromCliEnvVariable, isEnabled, setEnabled } from "./experiments"; + +describe("experiments", () => { + let originalCLIState = process.env.FIREBASE_CLI_EXPERIMENTS; + + before(() => { + originalCLIState = process.env.FIREBASE_CLI_EXPERIMENTS; + }); + + beforeEach(() => { + process.env.FIREBASE_CLI_EXPERIMENTS = originalCLIState; + }); + + afterEach(() => { + process.env.FIREBASE_CLI_EXPERIMENTS = originalCLIState; + }); + + describe("enableExperimentsFromCliEnvVariable", () => { + it("should enable some experiments", () => { + expect(isEnabled("experiments")).to.be.false; + process.env.FIREBASE_CLI_EXPERIMENTS = "experiments,not_an_experiment"; + + enableExperimentsFromCliEnvVariable(); + + expect(isEnabled("experiments")).to.be.true; + setEnabled("experiments", false); + }); + }); +}); diff --git a/src/experiments.ts b/src/experiments.ts new file mode 100644 index 00000000000..3d829983333 --- /dev/null +++ b/src/experiments.ts @@ -0,0 +1,254 @@ +import { bold, italic } from "colorette"; +import * as leven from "leven"; +import { basename } from "path"; + +import { configstore } from "./configstore"; +import { FirebaseError } from "./error"; +import { isRunningInGithubAction } from "./init/features/hosting/github"; + +export interface Experiment { + shortDescription: string; + fullDescription?: string; + public?: boolean; + docsUri?: string; + default?: boolean; +} + +// Utility method to ensure there are no typos in defining ALL_EXPERIMENTS +function experiments(exp: Record): Record { + return Object.freeze(exp); +} + +export const ALL_EXPERIMENTS = experiments({ + // meta: + experiments: { + shortDescription: "enables the experiments family of commands", + }, + + // Realtime Database experiments + rtdbrules: { + shortDescription: "Advanced security rules management", + }, + rtdbmanagement: { + shortDescription: "Use new endpoint to administer realtime database instances", + }, + // Cloud Functions for Firebase experiments + functionsv2deployoptimizations: { + shortDescription: "Optimize deployments of v2 firebase functions", + fullDescription: + "Reuse build images across funtions to increase performance and reliaibility " + + "of deploys. This has been made an experiment due to backend bugs that are " + + "temporarily causing failures in some regions with this optimization enabled", + public: true, + default: false, + }, + deletegcfartifacts: { + shortDescription: `Add the ${bold( + "functions:deletegcfartifacts", + )} command to purge docker build images`, + fullDescription: + `Add the ${bold("functions:deletegcfartifacts")}` + + "command. Google Cloud Functions creates Docker images when building your " + + "functions. Cloud Functions for Firebase automatically cleans up these " + + "images for you on deploy. Customers who predated this cleanup, or customers " + + "who also deploy Google Cloud Functions with non-Firebase tooling may have " + + "old Docker images stored in either Google Container Repository or Artifact " + + `Registry. The ${bold("functions:deletegcfartifacts")} command ` + + "will delete all Docker images created by Google Cloud Functions irrespective " + + "of how that image was created.", + public: true, + }, + + // permanent experiment + automaticallydeletegcfartifacts: { + shortDescription: "Control whether functions cleans up images after deploys", + fullDescription: + "To control costs, Firebase defaults to automatically deleting containers " + + "created during the build process. This has the side-effect of preventing " + + "users from rolling back to previous revisions using the Run API. To change " + + `this behavior, call ${bold("experiments:disable deletegcfartifactsondeploy")} ` + + `consider also calling ${bold("experiments:enable deletegcfartifacts")} ` + + `to enable the new command ${bold("functions:deletegcfartifacts")} which` + + "lets you clean up images manually", + public: true, + default: true, + }, + + // Emulator experiments + emulatoruisnapshot: { + shortDescription: "Load pre-release versions of the emulator UI", + }, + + // Hosting experiments + webframeworks: { + shortDescription: "Native support for popular web frameworks", + fullDescription: + "Adds support for popular web frameworks such as Next.js " + + "Angular, React, Svelte, and Vite-compatible frameworks. A manual migration " + + "may be required when the non-experimental support for these frameworks " + + "is released", + docsUri: "https://firebase.google.com/docs/hosting/frameworks-overview", + public: true, + }, + pintags: { + shortDescription: "Adds the pinTag option to Run and Functions rewrites", + fullDescription: + "Adds support for the 'pinTag' boolean on Runction and Run rewrites for " + + "Firebase Hosting. With this option, newly released hosting sites will be " + + "bound to the current latest version of their referenced functions or services. " + + "This option depends on Run pinned traffic targets, of which only 2000 can " + + "exist per region. firebase-tools aggressively garbage collects tags it creates " + + "if any service exceeds 500 tags, but it is theoretically possible that a project " + + "exceeds the region-wide limit of tags and an old site version fails", + public: true, + default: true, + }, + // Access experiments + crossservicerules: { + shortDescription: "Allow Firebase Rules to reference resources in other services", + }, + internaltesting: { + shortDescription: "Exposes Firebase CLI commands intended for internal testing purposes.", + fullDescription: + "Exposes Firebase CLI commands intended for internal testing purposes. " + + "These commands are not meant for public consumption and may break or disappear " + + "without a notice.", + }, + + apphosting: { + shortDescription: "Allow CLI option for Frameworks", + default: true, + public: false, + }, + + // TODO(joehanley): Delete this once weve scrubbed all references to experiment from docs. + dataconnect: { + shortDescription: "Deprecated. Previosuly, enabled Data Connect related features.", + fullDescription: "Deprecated. Previously, enabled Data Connect related features.", + public: false, + }, + + genkit: { + shortDescription: "Enable Genkit related features.", + fullDescription: "Enable Genkit related features.", + default: true, + public: false, + }, +}); + +export type ExperimentName = keyof typeof ALL_EXPERIMENTS; + +/** Determines whether a name is a valid experiment name. */ +export function isValidExperiment(name: string): name is ExperimentName { + return Object.keys(ALL_EXPERIMENTS).includes(name); +} + +/** + * Detects experiment names that were potentially what a customer intended to + * type when they provided malformed. + * Returns null if the malformed name is actually an experiment. Returns all + * possible typos. + */ +export function experimentNameAutocorrect(malformed: string): string[] { + if (isValidExperiment(malformed)) { + throw new FirebaseError( + "Assertion failed: experimentNameAutocorrect given actual experiment name", + { exit: 2 }, + ); + } + + // N.B. I personally would use < (name.length + malformed.length) * 0.2 + // but this logic matches src/index.ts. I neither want to change something + // with such potential impact nor to create divergent behavior. + return Object.keys(ALL_EXPERIMENTS).filter( + (name) => leven(name, malformed) < malformed.length * 0.4, + ); +} + +let localPreferencesCache: Record | undefined = undefined; +function localPreferences(): Record { + if (!localPreferencesCache) { + localPreferencesCache = (configstore.get("previews") || {}) as Record; + for (const key of Object.keys(localPreferencesCache)) { + if (!isValidExperiment(key)) { + delete localPreferencesCache[key as ExperimentName]; + } + } + } + return localPreferencesCache; +} + +/** Returns whether an experiment is enabled. */ +export function isEnabled(name: ExperimentName): boolean { + return localPreferences()[name] ?? ALL_EXPERIMENTS[name]?.default ?? false; +} + +/** + * Sets whether an experiment is enabled. + * Set to a boolean value to explicitly opt in or out of an experiment. + * Set to null to go on the default track for this experiment. + */ +export function setEnabled(name: ExperimentName, to: boolean | null): void { + if (to === null) { + delete localPreferences()[name]; + } else { + localPreferences()[name] = to; + } +} + +/** + * Enables multiple experiments given a comma-delimited environment variable: + * `FIREBASE_CLI_EXPERIMENTS`. + * + * Example: + * FIREBASE_CLI_PREVIEWS=experiment1,experiment2,turtle + * + * Would silently enable `experiment1` and `experiment2`, but would not enable `turtle`. + */ +export function enableExperimentsFromCliEnvVariable(): void { + const experiments = process.env.FIREBASE_CLI_EXPERIMENTS || ""; + for (const experiment of experiments.split(",")) { + if (isValidExperiment(experiment)) { + setEnabled(experiment, true); + } + } +} + +/** + * Assert that an experiment is enabled before following a code path. + * This code is unnecessary in code paths guarded by ifEnabled. When + * a customer's project was clearly written against an experiment that + * was not enabled, assertEnabled will throw a standard error. The "task" + * param is part of this error. It will be presented as "Cannot ${task}". + */ +export function assertEnabled(name: ExperimentName, task: string): void { + if (!isEnabled(name)) { + const prefix = `Cannot ${task} because the experiment ${bold(name)} is not enabled.`; + if (isRunningInGithubAction()) { + const path = process.env.GITHUB_WORKFLOW_REF?.split("@")[0]; + const filename = path ? `.github/workflows/${basename(path)}` : "your action's yml"; + const newValue = [process.env.FIREBASE_CLI_EXPERIMENTS, name].filter((it) => !!it).join(","); + throw new FirebaseError( + `${prefix} To enable add a ${bold( + "FIREBASE_CLI_EXPERIMENTS", + )} environment variable to ${filename}, like so: ${italic(` + +- uses: FirebaseExtended/action-hosting-deploy@v0 + with: + ... + env: + FIREBASE_CLI_EXPERIMENTS: ${newValue} +`)}`, + ); + } else { + throw new FirebaseError( + `${prefix} To enable ${bold(name)} run ${bold(`firebase experiments:enable ${name}`)}`, + ); + } + } +} + +/** Saves the current set of enabled experiments to disk. */ +export function flushToDisk(): void { + configstore.set("previews", localPreferences()); +} diff --git a/src/extensions/askUserForConsent.ts b/src/extensions/askUserForConsent.ts deleted file mode 100644 index f3c2c50b799..00000000000 --- a/src/extensions/askUserForConsent.ts +++ /dev/null @@ -1,116 +0,0 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as marked from "marked"; -import TerminalRenderer = require("marked-terminal"); - -import { FirebaseError } from "../error"; -import { logPrefix } from "../extensions/extensionsHelper"; -import * as extensionsApi from "./extensionsApi"; -import * as iam from "../gcp/iam"; -import { promptOnce, Question } from "../prompt"; -import * as utils from "../utils"; - -marked.setOptions({ - renderer: new TerminalRenderer(), -}); - -/** - * Returns a string that will be displayed in the prompt to user. - * @param extensionName name or ID of the extension (i.e. firestore-bigquery-export) - * @param projectId ID for the project where we are trying to install an extension into - * @param roles the role(s) we would like to grant to the service account managing the extension - * @return {string} description of roles to prompt user for permission - */ -export async function formatDescription(extensionName: string, projectId: string, roles: string[]) { - const question = `${clc.bold( - extensionName - )} will be granted the following access to project ${clc.bold(projectId)}`; - const results: string[] = await Promise.all( - roles.map((role: string) => { - return retrieveRoleInfo(role); - }) - ); - results.unshift(question); - return _.join(results, "\n"); -} - -/** - * Returns a string representing a Role, see - * https://cloud.google.com/iam/reference/rest/v1/organizations.roles#Role - * for more details on parameters of a Role. - * @param role to get info for - * @return {string} string representation for role - */ -export async function retrieveRoleInfo(role: string) { - const res = await iam.getRole(role); - return `- ${res.title} (${res.description})`; -} - -/** - * Displays roles that will be granted to the extension instance and corresponding descriptions. - * @param extensionName name of extension to install/update - * @param projectId ID of user's project - * @param roles roles that require user approval - */ -export async function displayRoles( - extensionName: string, - projectId: string, - roles: string[] -): Promise { - if (!roles.length) { - return; - } - - const message = await formatDescription(extensionName, projectId, roles); - utils.logLabeledBullet(logPrefix, message); -} - -/** - * Displays APIs that will be enabled for the project and corresponding descriptions. - * @param extensionName name of extension to install/update - * @param projectId ID of user's project - * @param apis APIs that require user approval - */ -export function displayApis(extensionName: string, projectId: string, apis: extensionsApi.Api[]) { - if (!apis.length) { - return; - } - const question = `${clc.bold( - extensionName - )} will enable the following APIs for project ${clc.bold(projectId)}`; - const results: string[] = apis.map((api: extensionsApi.Api) => { - return `- ${api.apiName}: ${api.reason}`; - }); - results.unshift(question); - const message = results.join("\n"); - utils.logLabeledBullet(logPrefix, message); -} - -/** - * Displays publisher terms of service and asks user to consent to them. - * Errors if they do not consent. - */ -export async function promptForPublisherTOS() { - const termsOfServiceMsg = - "By registering as a publisher, you confirm that you have read the Firebase Extensions Publisher Terms and Conditions (linked below) and you, on behalf of yourself and the organization you represent, agree to comply with it. Here is a brief summary of the highlights of our terms and conditions:\n" + - " - You ensure extensions you publish comply with all laws and regulations; do not include any viruses, spyware, Trojan horses, or other malicious code; and do not violate any person’s rights, including intellectual property, privacy, and security rights.\n" + - " - You will not engage in any activity that interferes with or accesses in an unauthorized manner the properties or services of Google, Google’s affiliates, or any third party.\n" + - " - If you become aware or should be aware of a critical security issue in your extension, you will provide either a resolution or a written resolution plan within 48 hours.\n" + - " - If Google requests a critical security matter to be patched for your extension, you will respond to Google within 48 hours with either a resolution or a written resolution plan.\n" + - " - Google may remove your extension or terminate the agreement, if you violate any terms."; - utils.logLabeledBullet(logPrefix, marked(termsOfServiceMsg)); - const question: Question = { - name: "consent", - type: "confirm", - message: marked( - "Do you accept the [Firebase Extensions Publisher Terms and Conditions](https://firebase.google.com/docs/extensions/alpha/terms-of-service) and acknowledge that your information will be used in accordance with [Google's Privacy Policy](https://policies.google.com/privacy?hl=en)?" - ), - default: false, - }; - const consented: boolean = await promptOnce(question); - if (!consented) { - throw new FirebaseError("You must agree to the terms of service to register a publisher ID.", { - exit: 1, - }); - } -} diff --git a/src/extensions/askUserForEventsConfig.spec.ts b/src/extensions/askUserForEventsConfig.spec.ts new file mode 100644 index 00000000000..da2d802e174 --- /dev/null +++ b/src/extensions/askUserForEventsConfig.spec.ts @@ -0,0 +1,83 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { + askForEventArcLocation, + askForAllowedEventTypes, + checkAllowedEventTypesResponse, +} from "./askUserForEventsConfig"; +import * as utils from "../utils"; +import * as prompt from "../prompt"; + +describe("checkAllowedEventTypesResponse", () => { + let logWarningSpy: sinon.SinonSpy; + beforeEach(() => { + logWarningSpy = sinon.spy(utils, "logWarning"); + }); + + afterEach(() => { + logWarningSpy.restore(); + }); + + it("should return false if allowed events is not part of extension spec's events list", () => { + expect( + checkAllowedEventTypesResponse( + ["google.firebase.nonexistent-event-occurred"], + [{ type: "google.firebase.custom-event-occurred", description: "A custom event occurred" }], + ), + ).to.equal(false); + expect( + logWarningSpy.calledWith( + "Unexpected event type 'google.firebase.nonexistent-event-occurred' was configured to be emitted. This event type is not part of the extension spec.", + ), + ).to.equal(true); + }); + + it("should return true if every allowed event exists in extension spec's events list", () => { + expect( + checkAllowedEventTypesResponse( + ["google.firebase.custom-event-occurred"], + [{ type: "google.firebase.custom-event-occurred", description: "A custom event occurred" }], + ), + ).to.equal(true); + }); +}); + +describe("askForAllowedEventTypes", () => { + let promptStub: sinon.SinonStub; + + afterEach(() => { + promptStub.restore(); + }); + + it("should keep prompting user until valid input is given", async () => { + promptStub = sinon.stub(prompt, "promptOnce"); + promptStub.onCall(0).returns(["invalid"]); + promptStub.onCall(1).returns(["stillinvalid"]); + promptStub.onCall(2).returns(["google.firebase.custom-event-occurred"]); + await askForAllowedEventTypes([ + { type: "google.firebase.custom-event-occurred", description: "A custom event occurred" }, + ]); + expect(promptStub.calledThrice).to.be.true; + }); +}); + +describe("askForEventarcLocation", () => { + let promptStub: sinon.SinonStub; + + beforeEach(() => { + promptStub = sinon.stub(prompt, "promptOnce"); + promptStub.onCall(0).returns("invalid-region"); + promptStub.onCall(1).returns("still-invalid-region"); + promptStub.onCall(2).returns("us-central1"); + }); + + afterEach(() => { + promptStub.restore(); + }); + + it("should keep prompting user until valid input is given", async () => { + await askForEventArcLocation(); + expect(promptStub.calledThrice).to.be.true; + }); +}); diff --git a/src/extensions/askUserForEventsConfig.ts b/src/extensions/askUserForEventsConfig.ts new file mode 100644 index 00000000000..9b7529fe08d --- /dev/null +++ b/src/extensions/askUserForEventsConfig.ts @@ -0,0 +1,121 @@ +import { promptOnce } from "../prompt"; +import * as extensionsApi from "../extensions/extensionsApi"; +import { EventDescriptor, ExtensionInstance } from "./types"; +import * as utils from "../utils"; +import * as clc from "colorette"; +import { logger } from "../logger"; +import { marked } from "marked"; + +export interface InstanceEventsConfig { + channel: string; + allowedEventTypes: string[]; +} + +export function checkAllowedEventTypesResponse( + response: string[], + validEvents: EventDescriptor[], +): boolean { + const validEventTypes = validEvents.map((e) => e.type); + if (response.length === 0) { + return false; + } + for (const e of response) { + if (!validEventTypes.includes(e)) { + utils.logWarning( + `Unexpected event type '${e}' was configured to be emitted. This event type is not part of the extension spec.`, + ); + return false; + } + } + return true; +} + +export async function askForEventsConfig( + events: EventDescriptor[], + projectId: string, + instanceId: string, +): Promise { + logger.info( + `\n${clc.bold("Enable Events")}: ${marked( + "If you enable events, you can write custom event handlers ([https://firebase.google.com/docs/extensions/install-extensions#eventarc](https://firebase.google.com/docs/extensions/install-extensions#eventarc)) that respond to these events.\n\nYou can always enable or disable events later. Events will be emitted via Eventarc. Fees apply ([https://cloud.google.com/eventarc/pricing](https://cloud.google.com/eventarc/pricing)).", + )}`, + ); + if (!(await askShouldCollectEventsConfig())) { + return undefined; + } + let existingInstance: ExtensionInstance | undefined; + try { + existingInstance = instanceId + ? await extensionsApi.getInstance(projectId, instanceId) + : undefined; + } catch { + /* If instance was not found, then this is an instance ID for a new instance. Don't preselect any values when displaying prompts to the user. */ + } + const preselectedTypes = existingInstance?.config.allowedEventTypes ?? []; + const oldLocation = existingInstance?.config.eventarcChannel?.split("/")[3]; + const location = await askForEventArcLocation(oldLocation); + const channel = `projects/${projectId}/locations/${location}/channels/firebase`; + const allowedEventTypes = await askForAllowedEventTypes(events, preselectedTypes); + return { channel, allowedEventTypes }; +} + +export async function askForAllowedEventTypes( + eventDescriptors: EventDescriptor[], + preselectedTypes?: string[], +): Promise { + let valid = false; + let response: string[] = []; + const eventTypes = eventDescriptors.map((e, index) => ({ + checked: false, + name: `${index + 1}. ${e.type}\n ${e.description}`, + value: e.type, + })); + while (!valid) { + response = await promptOnce({ + name: "selectedEventTypesInput", + type: "checkbox", + default: preselectedTypes ?? [], + message: + `Please select the events [${eventTypes.length} types total] that this extension is permitted to emit. ` + + "You can implement your own handlers that trigger when these events are emitted to customize the extension's behavior. ", + choices: eventTypes, + pageSize: 20, + }); + valid = checkAllowedEventTypesResponse(response, eventDescriptors); + } + return response.filter((e) => e !== ""); +} + +export async function askShouldCollectEventsConfig(): Promise { + return promptOnce({ + type: "confirm", + name: "shouldCollectEvents", + message: `Would you like to enable events?`, + default: false, + }); +} + +export async function askForEventArcLocation(preselectedLocation?: string): Promise { + let valid = false; + const allowedRegions = ["us-central1", "us-west1", "europe-west4", "asia-northeast1"]; + let location = ""; + while (!valid) { + location = await promptOnce({ + name: "input", + type: "list", + default: preselectedLocation ?? "us-central1", + message: + "Which location would you like the Eventarc channel to live in? We recommend using the default option. A channel location that differs from the extension's Cloud Functions location can incur egress cost.", + choices: allowedRegions.map((e) => ({ checked: false, value: e })), + }); + valid = allowedRegions.includes(location); + if (!valid) { + utils.logWarning( + `Unexpected EventArc region '${location}' was specified. Allowed regions: ${allowedRegions.join( + ", ", + )}`, + ); + } + } + return location; +} diff --git a/src/extensions/askUserForParam.spec.ts b/src/extensions/askUserForParam.spec.ts new file mode 100644 index 00000000000..c46dbd59a34 --- /dev/null +++ b/src/extensions/askUserForParam.spec.ts @@ -0,0 +1,386 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { + ask, + askForParam, + checkResponse, + getInquirerDefault, + SecretLocation, +} from "./askUserForParam"; +import * as utils from "../utils"; +import * as prompt from "../prompt"; +import { ParamType } from "./types"; +import * as extensionsHelper from "./extensionsHelper"; +import * as secretManagerApi from "../gcp/secretManager"; +import * as secretsUtils from "./secretsUtils"; + +describe("askUserForParam", () => { + const testSpec = { + param: "NAME", + type: ParamType.STRING, + label: "Name", + default: "Lauren", + validationRegex: "^[a-z,A-Z]*$", + }; + + describe("checkResponse", () => { + let logWarningSpy: sinon.SinonSpy; + beforeEach(() => { + logWarningSpy = sinon.spy(utils, "logWarning"); + }); + + afterEach(() => { + logWarningSpy.restore(); + }); + + it("should return false if required variable is not set", () => { + expect( + checkResponse("", { + param: "param", + label: "fill in the blank!", + type: ParamType.STRING, + required: true, + }), + ).to.equal(false); + expect( + logWarningSpy.calledWith(`Param param is required, but no value was provided.`), + ).to.equal(true); + }); + + it("should return false if regex validation fails", () => { + expect( + checkResponse("123", { + param: "param", + label: "fill in the blank!", + type: ParamType.STRING, + validationRegex: "foo", + required: true, + }), + ).to.equal(false); + const expectedWarning = `123 is not a valid value for param since it does not meet the requirements of the regex validation: "foo"`; + expect(logWarningSpy.calledWith(expectedWarning)).to.equal(true); + }); + + it("should return false if regex validation fails on an optional param that is not empty", () => { + expect( + checkResponse("123", { + param: "param", + label: "fill in the blank!", + type: ParamType.STRING, + validationRegex: "foo", + required: false, + }), + ).to.equal(false); + const expectedWarning = `123 is not a valid value for param since it does not meet the requirements of the regex validation: "foo"`; + expect(logWarningSpy.calledWith(expectedWarning)).to.equal(true); + }); + + it("should return true if no value is passed for an optional param", () => { + expect( + checkResponse("", { + param: "param", + label: "fill in the blank!", + type: ParamType.STRING, + validationRegex: "foo", + required: false, + }), + ).to.equal(true); + }); + + it("should not check against list of options if no value is passed for an optional SELECT", () => { + expect( + checkResponse("", { + param: "param", + label: "fill in the blank!", + type: ParamType.SELECT, + required: false, + options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], + }), + ).to.equal(true); + }); + + it("should not check against list of options if no value is passed for an optional MULTISELECT", () => { + expect( + checkResponse("", { + param: "param", + label: "fill in the blank!", + type: ParamType.MULTISELECT, + required: false, + options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], + }), + ).to.equal(true); + }); + + it("should use custom validation error message if provided", () => { + const message = "please enter a word with foo in it"; + expect( + checkResponse("123", { + param: "param", + label: "fill in the blank!", + type: ParamType.STRING, + validationRegex: "foo", + validationErrorMessage: message, + required: true, + }), + ).to.equal(false); + expect(logWarningSpy.calledWith(message)).to.equal(true); + }); + + it("should return true if all conditions pass", () => { + expect( + checkResponse("123", { + param: "param", + label: "fill in the blank!", + type: ParamType.STRING, + }), + ).to.equal(true); + expect(logWarningSpy.called).to.equal(false); + }); + + it("should return false if an invalid choice is selected", () => { + expect( + checkResponse("???", { + param: "param", + label: "pick one!", + type: ParamType.SELECT, + options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], + }), + ).to.equal(false); + }); + + it("should return true if an valid choice is selected", () => { + expect( + checkResponse("aaa", { + param: "param", + label: "pick one!", + type: ParamType.SELECT, + options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], + }), + ).to.equal(true); + }); + + it("should return false if multiple invalid choices are selected", () => { + expect( + checkResponse("d,e,f", { + param: "param", + label: "pick multiple!", + type: ParamType.MULTISELECT, + options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], + }), + ).to.equal(false); + }); + + it("should return true if one valid choice is selected", () => { + expect( + checkResponse("ccc", { + param: "param", + label: "pick multiple!", + type: ParamType.MULTISELECT, + options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], + }), + ).to.equal(true); + }); + + it("should return true if multiple valid choices are selected", () => { + expect( + checkResponse("aaa,bbb,ccc", { + param: "param", + label: "pick multiple!", + type: ParamType.MULTISELECT, + options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], + }), + ).to.equal(true); + }); + }); + + describe("getInquirerDefaults", () => { + it("should return the label of the option whose value matches the default", () => { + const options = [ + { label: "lab", value: "val" }, + { label: "lab1", value: "val1" }, + ]; + const def = "val1"; + + const res = getInquirerDefault(options, def); + + expect(res).to.equal("lab1"); + }); + + it("should return the value of the default option if it doesnt have a label", () => { + const options = [{ label: "lab", value: "val" }, { value: "val1" }]; + const def = "val1"; + + const res = getInquirerDefault(options, def); + + expect(res).to.equal("val1"); + }); + + it("should return an empty string if a default option is not found", () => { + const options = [{ label: "lab", value: "val" }, { value: "val1" }]; + const def = "val2"; + + const res = getInquirerDefault(options, def); + + expect(res).to.equal(""); + }); + }); + describe("askForParam with string param", () => { + let promptStub: sinon.SinonStub; + + beforeEach(() => { + promptStub = sinon.stub(prompt, "promptOnce"); + promptStub.onCall(0).returns("Invalid123"); + promptStub.onCall(1).returns("InvalidStill123"); + promptStub.onCall(2).returns("ValidName"); + }); + + afterEach(() => { + promptStub.restore(); + }); + + it("should keep prompting user until valid input is given", async () => { + await askForParam({ + projectId: "project-id", + instanceId: "instance-id", + paramSpec: testSpec, + reconfiguring: false, + }); + expect(promptStub.calledThrice).to.be.true; + }); + }); + + describe("askForParam with secret param", () => { + const stubSecret = { + name: "new-secret", + projectId: "firebase-project-123", + }; + const stubSecretVersion = { + secret: stubSecret, + versionId: "1.0.0", + }; + const secretSpec = { + param: "API_KEY", + type: ParamType.SECRET, + label: "API Key", + default: "XXX.YYY", + }; + + let promptStub: sinon.SinonStub; + let createSecret: sinon.SinonStub; + let secretExists: sinon.SinonStub; + let addVersion: sinon.SinonStub; + let grantRole: sinon.SinonStub; + + beforeEach(() => { + promptStub = sinon.stub(prompt, "promptOnce"); + secretExists = sinon.stub(secretManagerApi, "secretExists"); + createSecret = sinon.stub(secretManagerApi, "createSecret"); + addVersion = sinon.stub(secretManagerApi, "addVersion"); + grantRole = sinon.stub(secretsUtils, "grantFirexServiceAgentSecretAdminRole"); + + secretExists.onCall(0).resolves(false); + createSecret.onCall(0).resolves(stubSecret); + addVersion.onCall(0).resolves(stubSecretVersion); + grantRole.onCall(0).resolves(undefined); + }); + + afterEach(() => { + promptStub.restore(); + secretExists.restore(); + createSecret.restore(); + addVersion.restore(); + grantRole.restore(); + }); + + it("should return the correct user input for secret stored with Secret Manager", async () => { + promptStub.onCall(0).returns([SecretLocation.CLOUD.toString()]); + promptStub.onCall(1).returns("ABC.123"); + + const result = await askForParam({ + projectId: "project-id", + instanceId: "instance-id", + paramSpec: secretSpec, + reconfiguring: false, + }); + + // prompt for secret storage location, then prompt for secret value + expect(promptStub.calledTwice).to.be.true; + expect(grantRole.calledOnce).to.be.true; + expect(result).to.be.eql({ + baseValue: `projects/${stubSecret.projectId}/secrets/${stubSecret.name}/versions/${stubSecretVersion.versionId}`, + }); + }); + + it("should return the correct user input for secret stored in a local file", async () => { + promptStub.onCall(0).returns([SecretLocation.LOCAL.toString()]); + promptStub.onCall(1).returns("ABC.123"); + + const result = await askForParam({ + projectId: "project-id", + instanceId: "instance-id", + paramSpec: secretSpec, + reconfiguring: false, + }); + // prompt for secret storage location, then prompt for secret value + expect(promptStub.calledTwice).to.be.true; + // Shouldn't make any api calls. + expect(grantRole.calledOnce).to.be.false; + expect(result).to.be.eql({ + baseValue: "", + local: "ABC.123", + }); + }); + + it("should handle cloud & local secret storage at the same time", async () => { + promptStub + .onCall(0) + .returns([SecretLocation.CLOUD.toString(), SecretLocation.LOCAL.toString()]); + promptStub.onCall(1).returns("ABC.123"); + promptStub.onCall(2).returns("LOCAL.ABC.123"); + + const result = await askForParam({ + projectId: "project-id", + instanceId: "instance-id", + paramSpec: secretSpec, + reconfiguring: false, + }); + // prompt for secret storage location, then prompt for cloud secret value, then local + expect(promptStub.calledThrice).to.be.true; + expect(grantRole.calledOnce).to.be.true; + expect(result).to.be.eql({ + baseValue: `projects/${stubSecret.projectId}/secrets/${stubSecret.name}/versions/${stubSecretVersion.versionId}`, + local: "LOCAL.ABC.123", + }); + }); + }); + + describe("ask", () => { + let subVarSpy: sinon.SinonSpy; + let promptStub: sinon.SinonStub; + + beforeEach(() => { + subVarSpy = sinon.spy(extensionsHelper, "substituteParams"); + promptStub = sinon.stub(prompt, "promptOnce"); + promptStub.returns("ValidName"); + }); + + afterEach(() => { + subVarSpy.restore(); + promptStub.restore(); + }); + + it("should call substituteParams with the right parameters", async () => { + const spec = [testSpec]; + const firebaseProjectVars = { PROJECT_ID: "my-project" }; + await ask({ + projectId: "project-id", + instanceId: "instance-id", + paramSpecs: spec, + firebaseProjectParams: firebaseProjectVars, + reconfiguring: false, + }); + expect(subVarSpy.calledWith(spec, firebaseProjectVars)).to.be.true; + }); + }); +}); diff --git a/src/extensions/askUserForParam.ts b/src/extensions/askUserForParam.ts index 94ba1b7d4c3..5c7efdecedc 100644 --- a/src/extensions/askUserForParam.ts +++ b/src/extensions/askUserForParam.ts @@ -1,8 +1,8 @@ import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as marked from "marked"; +import * as clc from "colorette"; +import { marked } from "marked"; -import { Param, ParamOption, ParamType } from "./extensionsApi"; +import { Param, ParamOption, ParamType } from "./types"; import * as secretManagerApi from "../gcp/secretManager"; import * as secretsUtils from "./secretsUtils"; import { logPrefix, substituteParams } from "./extensionsHelper"; @@ -10,9 +10,22 @@ import { convertExtensionOptionToLabeledList, getRandomString, onceWithJoin } fr import { logger } from "../logger"; import { promptOnce } from "../prompt"; import * as utils from "../utils"; +import { ParamBindingOptions } from "./paramHelper"; +import { needProjectId } from "../projectUtils"; +import { partition } from "../functional"; + +/** + * Location where the secret value is stored. + * + * Visible for testing. + */ +export enum SecretLocation { + CLOUD = 1, + LOCAL, +} enum SecretUpdateAction { - LEAVE, + LEAVE = 1, SET_NEW, } @@ -20,7 +33,7 @@ export function checkResponse(response: string, spec: Param): boolean { let valid = true; let responses: string[]; - if (spec.required && (response == "" || response == undefined)) { + if (spec.required && (response === "" || response === undefined)) { utils.logWarning(`Param ${spec.param} is required, but no value was provided.`); return false; } @@ -34,7 +47,7 @@ export function checkResponse(response: string, spec: Param): boolean { if (spec.validationRegex && !!response) { // !!response to ignore empty optional params const re = new RegExp(spec.validationRegex); - _.forEach(responses, (resp) => { + for (const resp of responses) { if ((spec.required || resp !== "") && !re.test(resp)) { const genericWarn = `${resp} is not a valid value for ${spec.param} since it` + @@ -42,38 +55,107 @@ export function checkResponse(response: string, spec: Param): boolean { utils.logWarning(spec.validationErrorMessage || genericWarn); valid = false; } - }); + } } if (spec.type && (spec.type === ParamType.MULTISELECT || spec.type === ParamType.SELECT)) { - _.forEach(responses, (r) => { + for (const r of responses) { // A choice is valid if it matches one of the option values. - const validChoice = _.some(spec.options, (option: ParamOption) => { - return r === option.value; - }); - if (!validChoice) { + const validChoice = spec.options?.some((option) => r === option.value); + if (r && !validChoice) { utils.logWarning(`${r} is not a valid option for ${spec.param}.`); valid = false; } - }); + } } return valid; } -export async function askForParam( - projectId: string, - instanceId: string, - paramSpec: Param, - reconfiguring: boolean -): Promise { +/** + * Prompt users for params based on paramSpecs defined by the extension developer. + * @param paramSpecs Array of params to ask the user about, parsed from extension.yaml. + * @param firebaseProjectParams Autopopulated Firebase project-specific params + * @return Promisified map of env vars to values. + */ +export async function ask(args: { + projectId: string | undefined; + instanceId: string; + paramSpecs: Param[]; + firebaseProjectParams: { [key: string]: string }; + reconfiguring: boolean; +}): Promise<{ [key: string]: ParamBindingOptions }> { + if (_.isEmpty(args.paramSpecs)) { + logger.debug("No params were specified for this extension."); + return {}; + } + + utils.logLabeledBullet(logPrefix, "answer the questions below to configure your extension:"); + const substituted = substituteParams(args.paramSpecs, args.firebaseProjectParams); + const [advancedParams, standardParams] = partition(substituted, (p) => p.advanced ?? false); + const result: { [key: string]: ParamBindingOptions } = {}; + const promises = standardParams.map((paramSpec) => { + return async () => { + result[paramSpec.param] = await askForParam({ + projectId: args.projectId, + instanceId: args.instanceId, + paramSpec: paramSpec, + reconfiguring: args.reconfiguring, + }); + }; + }); + if (advancedParams.length) { + promises.push(async () => { + const shouldPrompt = await promptOnce({ + type: "confirm", + message: "Do you want to configure any advanced parameters for this instance?", + default: false, + }); + if (shouldPrompt) { + const advancedPromises = advancedParams.map((paramSpec) => { + return async () => { + result[paramSpec.param] = await askForParam({ + projectId: args.projectId, + instanceId: args.instanceId, + paramSpec: paramSpec, + reconfiguring: args.reconfiguring, + }); + }; + }); + await advancedPromises.reduce((prev, cur) => prev.then(cur as any), Promise.resolve()); + } else { + for (const paramSpec of advancedParams) { + if (paramSpec.required && paramSpec.default) { + result[paramSpec.param] = { baseValue: paramSpec.default }; + } + } + } + }); + } + // chaining together the promises so they get executed one after another + await promises.reduce((prev, cur) => prev.then(cur as any), Promise.resolve()); + + logger.info(); + return result; +} + +export async function askForParam(args: { + projectId?: string; + instanceId: string; + paramSpec: Param; + reconfiguring: boolean; +}): Promise { + const paramSpec = args.paramSpec; + let valid = false; let response = ""; + let responseForLocal; + let secretLocations: string[] = []; const description = paramSpec.description || ""; const label = paramSpec.label.trim(); logger.info( `\n${clc.bold(label)}${clc.bold(paramSpec.required ? "" : " (Optional)")}: ${marked( - description - ).trim()}` + description, + ).trim()}`, ); while (!valid) { @@ -109,16 +191,26 @@ export async function askForParam( }, message: "Which options do you want enabled for this parameter? " + - "Press Space to select, then Enter to confirm your choices. " + - "You may select multiple options.", + "Press Space to select, then Enter to confirm your choices. ", choices: convertExtensionOptionToLabeledList(paramSpec.options as ParamOption[]), }); valid = checkResponse(response, paramSpec); break; case ParamType.SECRET: - response = reconfiguring - ? await promptReconfigureSecret(projectId, instanceId, paramSpec) - : await promptCreateSecret(projectId, instanceId, paramSpec); + do { + secretLocations = await promptSecretLocations(paramSpec); + } while (!isValidSecretLocations(secretLocations, paramSpec)); + + if (secretLocations.includes(SecretLocation.CLOUD.toString())) { + // TODO(lihes): evaluate the UX of this error message. + const projectId = needProjectId({ projectId: args.projectId }); + response = args.reconfiguring + ? await promptReconfigureSecret(projectId, args.instanceId, paramSpec) + : await promptCreateSecret(projectId, args.instanceId, paramSpec); + } + if (secretLocations.includes(SecretLocation.LOCAL.toString())) { + responseForLocal = await promptLocalSecret(args.instanceId, paramSpec); + } valid = true; break; default: @@ -132,13 +224,78 @@ export async function askForParam( valid = checkResponse(response, paramSpec); } } - return response; + return { baseValue: response, ...(responseForLocal ? { local: responseForLocal } : {}) }; +} + +function isValidSecretLocations(secretLocations: string[], paramSpec: Param): boolean { + if (paramSpec.required) { + return !!secretLocations.length; + } + return true; +} + +async function promptSecretLocations(paramSpec: Param): Promise { + if (paramSpec.required) { + return await promptOnce({ + name: "input", + type: "checkbox", + message: "Where would you like to store your secrets? You must select at least one value", + choices: [ + { + checked: true, + name: "Google Cloud Secret Manager (Used by deployed extensions and emulator)", + // return type of string is not actually enforced, need to manually convert. + value: SecretLocation.CLOUD.toString(), + }, + { + checked: false, + name: "Local file (Used by emulator only)", + value: SecretLocation.LOCAL.toString(), + }, + ], + }); + } + return await promptOnce({ + name: "input", + type: "checkbox", + message: + "Where would you like to store your secrets? " + + "If you don't want to set this optional secret, leave both options unselected to skip it", + choices: [ + { + checked: false, + name: "Google Cloud Secret Manager (Used by deployed extensions and emulator)", + // return type of string is not actually enforced, need to manually convert. + value: SecretLocation.CLOUD.toString(), + }, + { + checked: false, + name: "Local file (Used by emulator only)", + value: SecretLocation.LOCAL.toString(), + }, + ], + }); +} + +async function promptLocalSecret(instanceId: string, paramSpec: Param): Promise { + let value; + do { + utils.logLabeledBullet(logPrefix, "Configure a local secret value for Extensions Emulator"); + value = await promptOnce({ + name: paramSpec.param, + type: "input", + message: + `This secret will be stored in ./extensions/${instanceId}.secret.local.\n` + + `Enter value for "${paramSpec.label.trim()}" to be used by Extensions Emulator:`, + }); + } while (!value); + return value; } async function promptReconfigureSecret( projectId: string, instanceId: string, - paramSpec: Param + paramSpec: Param, ): Promise { const action = await promptOnce({ type: "list", @@ -172,7 +329,7 @@ async function promptReconfigureSecret( secret = await secretManagerApi.createSecret( projectId, secretName, - secretsUtils.getSecretLabels(instanceId) + secretsUtils.getSecretLabels(instanceId), ); } return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue); @@ -192,7 +349,7 @@ export async function promptCreateSecret( projectId: string, instanceId: string, paramSpec: Param, - secretName?: string + secretName?: string, ): Promise { const name = secretName ?? (await generateSecretName(projectId, instanceId, paramSpec.param)); const secretValue = await promptOnce({ @@ -209,7 +366,7 @@ export async function promptCreateSecret( const secret = await secretManagerApi.createSecret( projectId, name, - secretsUtils.getSecretLabels(instanceId) + secretsUtils.getSecretLabels(instanceId), ); return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue); } else { @@ -223,7 +380,7 @@ export async function promptCreateSecret( async function generateSecretName( projectId: string, instanceId: string, - paramName: string + paramName: string, ): Promise { let secretName = `ext-${instanceId}-${paramName}`; while (await secretManagerApi.secretExists(projectId, secretName)) { @@ -237,48 +394,14 @@ async function addNewSecretVersion( instanceId: string, secret: secretManagerApi.Secret, paramSpec: Param, - secretValue: string + secretValue: string, ) { - const version = await secretManagerApi.addVersion(secret, secretValue); + const version = await secretManagerApi.addVersion(projectId, secret.name, secretValue); await secretsUtils.grantFirexServiceAgentSecretAdminRole(secret); return `projects/${version.secret.projectId}/secrets/${version.secret.name}/versions/${version.versionId}`; } export function getInquirerDefault(options: ParamOption[], def: string): string { - const defaultOption = _.find(options, (option) => { - return option.value === def; - }); + const defaultOption = options.find((o) => o.value === def); return defaultOption ? defaultOption.label || defaultOption.value : ""; } - -/** - * Prompt users for params based on paramSpecs defined by the extension developer. - * @param paramSpecs Array of params to ask the user about, parsed from extension.yaml. - * @param firebaseProjectParams Autopopulated Firebase project-specific params - * @return Promisified map of env vars to values. - */ -export async function ask( - projectId: string, - instanceId: string, - paramSpecs: Param[], - firebaseProjectParams: { [key: string]: string }, - reconfiguring: boolean -): Promise<{ [key: string]: string }> { - if (_.isEmpty(paramSpecs)) { - logger.debug("No params were specified for this extension."); - return {}; - } - - utils.logLabeledBullet(logPrefix, "answer the questions below to configure your extension:"); - const substituted = substituteParams(paramSpecs, firebaseProjectParams); - const result: any = {}; - const promises = _.map(substituted, (paramSpec: Param) => { - return async () => { - result[paramSpec.param] = await askForParam(projectId, instanceId, paramSpec, reconfiguring); - }; - }); - // chaining together the promises so they get executed one after another - await promises.reduce((prev, cur) => prev.then(cur as any), Promise.resolve()); - logger.info(); - return result; -} diff --git a/src/extensions/billingMigrationHelper.spec.ts b/src/extensions/billingMigrationHelper.spec.ts new file mode 100644 index 00000000000..ee2a3841082 --- /dev/null +++ b/src/extensions/billingMigrationHelper.spec.ts @@ -0,0 +1,126 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { FirebaseError } from "../error"; +import * as nodejsMigrationHelper from "./billingMigrationHelper"; +import * as prompt from "../prompt"; +import { ExtensionSpec } from "./types"; +import { cloneDeep } from "../utils"; + +const NO_RUNTIME_SPEC: ExtensionSpec = { + name: "test", + specVersion: "v1beta", + displayName: "Old", + description: "descriptive", + version: "1.0.0", + license: "MIT", + resources: [ + { + name: "resource1", + type: "firebaseextensions.v1beta.function", + description: "desc", + properties: {}, + }, + ], + author: { authorName: "Tester" }, + contributors: [{ authorName: "Tester 2" }], + billingRequired: true, + sourceUrl: "test.com", + params: [], + systemParams: [], +}; + +const NODE8_SPEC: ExtensionSpec = { + name: "test", + specVersion: "v1beta", + displayName: "Old", + description: "descriptive", + version: "1.0.0", + license: "MIT", + resources: [ + { + name: "resource1", + type: "firebaseextensions.v1beta.function", + description: "desc", + properties: { runtime: "nodejs8" }, + }, + ], + author: { authorName: "Tester" }, + contributors: [{ authorName: "Tester 2" }], + billingRequired: true, + sourceUrl: "test.com", + params: [], + systemParams: [], +}; + +const NODE10_SPEC: ExtensionSpec = { + name: "test", + specVersion: "v1beta", + displayName: "Old", + description: "descriptive", + version: "1.0.0", + license: "MIT", + resources: [ + { + name: "resource1", + type: "firebaseextensions.v1beta.function", + description: "desc", + properties: { runtime: "nodejs10" }, + }, + ], + author: { authorName: "Tester" }, + contributors: [{ authorName: "Tester 2" }], + billingRequired: true, + sourceUrl: "test.com", + params: [], + systemParams: [], +}; + +describe("billingMigrationHelper", () => { + let promptStub: sinon.SinonStub; + beforeEach(() => { + promptStub = sinon.stub(prompt, "promptOnce"); + }); + + afterEach(() => { + promptStub.restore(); + }); + + describe("displayNode10CreateBillingNotice", () => { + it("should notify the user if the runtime requires nodejs10", async () => { + promptStub.resolves(true); + const newSpec = cloneDeep(NODE10_SPEC); + + await expect(nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true)).not.to.be + .rejected; + expect(promptStub.callCount).to.equal(1); + }); + + it("should notify the user if the runtime does not require nodejs (explicit)", async () => { + promptStub.resolves(true); + const newSpec = cloneDeep(NODE8_SPEC); + + await expect(nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true)).not.to.be + .rejected; + expect(promptStub.callCount).to.equal(0); + }); + + it("should notify the user if the runtime does not require nodejs (implicit)", async () => { + promptStub.resolves(true); + const newSpec = cloneDeep(NO_RUNTIME_SPEC); + + await expect(nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true)).not.to.be + .rejected; + expect(promptStub.callCount).to.equal(0); + }); + + it("should error if the user doesn't give consent", async () => { + promptStub.resolves(false); + const newSpec = cloneDeep(NODE10_SPEC); + + await expect( + nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true), + ).to.be.rejectedWith(FirebaseError, "Cancelled"); + }); + }); +}); diff --git a/src/extensions/billingMigrationHelper.ts b/src/extensions/billingMigrationHelper.ts index 4ad6b61fe85..1d9c9b98443 100644 --- a/src/extensions/billingMigrationHelper.ts +++ b/src/extensions/billingMigrationHelper.ts @@ -1,11 +1,12 @@ -import * as marked from "marked"; -import TerminalRenderer = require("marked-terminal"); +import { marked } from "marked"; +import * as TerminalRenderer from "marked-terminal"; import { FirebaseError } from "../error"; -import * as extensionsApi from "./extensionsApi"; +import { ExtensionSpec } from "./types"; import { logPrefix } from "./extensionsHelper"; import { promptOnce } from "../prompt"; import * as utils from "../utils"; +import { getResourceRuntime } from "./utils"; marked.setOptions({ renderer: new TerminalRenderer(), @@ -36,11 +37,11 @@ const defaultRuntimes: { [key: string]: string } = { v1beta: "nodejs8", }; -function hasRuntime(spec: extensionsApi.ExtensionSpec, runtime: string): boolean { +function hasRuntime(spec: ExtensionSpec, runtime: string): boolean { const specVersion = spec.specVersion || defaultSpecVersion; const defaultRuntime = defaultRuntimes[specVersion]; const resources = spec.resources || []; - return resources.some((r) => runtime === (r.properties?.runtime || defaultRuntime)); + return resources.some((r) => runtime === (getResourceRuntime(r) || defaultRuntime)); } /** @@ -50,8 +51,8 @@ function hasRuntime(spec: extensionsApi.ExtensionSpec, runtime: string): boolean * @param newSpec A extensionSpec to compare to */ export function displayNode10UpdateBillingNotice( - curSpec: extensionsApi.ExtensionSpec, - newSpec: extensionsApi.ExtensionSpec + curSpec: ExtensionSpec, + newSpec: ExtensionSpec, ): void { if (hasRuntime(curSpec, "nodejs8") && hasRuntime(newSpec, "nodejs10")) { utils.logLabeledWarning(logPrefix, marked(billingMsgUpdate)); @@ -65,8 +66,8 @@ export function displayNode10UpdateBillingNotice( * @param prompt If true, prompts user for confirmation */ export async function displayNode10CreateBillingNotice( - spec: extensionsApi.ExtensionSpec, - prompt: boolean + spec: ExtensionSpec, + prompt: boolean, ): Promise { if (hasRuntime(spec, "nodejs10")) { utils.logLabeledWarning(logPrefix, marked(billingMsgCreate)); diff --git a/src/extensions/change-log.spec.ts b/src/extensions/change-log.spec.ts new file mode 100644 index 00000000000..812ced93151 --- /dev/null +++ b/src/extensions/change-log.spec.ts @@ -0,0 +1,168 @@ +import * as chai from "chai"; +import { expect } from "chai"; +chai.use(require("chai-as-promised")); +import * as sinon from "sinon"; + +import * as changelog from "./change-log"; +import * as extensionApi from "./extensionsApi"; +import { ExtensionVersion } from "./types"; + +function testExtensionVersion(version: string, releaseNotes?: string): ExtensionVersion { + return { + name: `publishers/test/extensions/test/versions/${version}`, + ref: `test/test@${version}`, + state: "PUBLISHED", + hash: "abc123", + sourceDownloadUri: "https://google.com", + releaseNotes, + spec: { + name: "test", + version, + resources: [], + params: [], + systemParams: [], + sourceUrl: "https://google.com", + }, + }; +} + +describe("changelog", () => { + describe("GetReleaseNotesForUpdate", () => { + let listExtensionVersionStub: sinon.SinonStub; + + beforeEach(() => { + listExtensionVersionStub = sinon.stub(extensionApi, "listExtensionVersions"); + }); + + afterEach(() => { + listExtensionVersionStub.restore(); + }); + + it("should return release notes for each version in the update", async () => { + const extensionVersions: ExtensionVersion[] = [ + testExtensionVersion("0.1.1", "foo"), + testExtensionVersion("0.1.2", "bar"), + ]; + listExtensionVersionStub + .withArgs("test/test", `id<="0.1.2" AND id>"0.1.0"`) + .returns(extensionVersions); + const want = { + "0.1.1": "foo", + "0.1.2": "bar", + }; + + const got = await changelog.getReleaseNotesForUpdate({ + extensionRef: "test/test", + fromVersion: "0.1.0", + toVersion: "0.1.2", + }); + + expect(got).to.deep.equal(want); + }); + + it("should exclude versions that don't have releaseNotes", async () => { + const extensionVersions: ExtensionVersion[] = [ + testExtensionVersion("0.1.1", "foo"), + testExtensionVersion("0.1.2"), + ]; + listExtensionVersionStub + .withArgs("test/test", `id<="0.1.2" AND id>"0.1.0"`) + .resolves(extensionVersions); + const want = { + "0.1.1": "foo", + }; + + const got = await changelog.getReleaseNotesForUpdate({ + extensionRef: "test/test", + fromVersion: "0.1.0", + toVersion: "0.1.2", + }); + + expect(got).to.deep.equal(want); + }); + }); + + describe("breakingChangesInUpdate", () => { + const testCases: { + description: string; + in: string[]; + want: string[]; + }[] = [ + { + description: "should return no breaking changes", + in: ["0.1.0", "0.1.1", "0.1.2"], + want: [], + }, + { + description: "should return prerelease breaking change", + in: ["0.1.0", "0.1.1", "0.2.0"], + want: ["0.2.0"], + }, + { + description: "should return breaking change", + in: ["1.1.0", "1.1.1", "2.0.0"], + want: ["2.0.0"], + }, + { + description: "should return multiple breaking changes", + in: ["0.1.0", "0.2.1", "1.0.0"], + want: ["0.2.1", "1.0.0"], + }, + ]; + for (const testCase of testCases) { + it(testCase.description, () => { + const got = changelog.breakingChangesInUpdate(testCase.in); + + expect(got).to.deep.equal(testCase.want); + }); + } + }); + + describe("parseChangelog", () => { + const testCases: { + description: string; + in: string; + want: Record; + }[] = [ + { + description: "should split changelog by version", + in: "## Version 0.1.0\nNotes\n## Version 0.1.1\nNew notes", + want: { + "0.1.0": "Notes", + "0.1.1": "New notes", + }, + }, + { + description: "should ignore text not in a version", + in: "Some random words\n## Version 0.1.0\nNotes\n## Version 0.1.1\nNew notes", + want: { + "0.1.0": "Notes", + "0.1.1": "New notes", + }, + }, + { + description: "should handle prerelease versions", + in: "Some random words\n## Version 0.1.0-rc.1\nNotes\n## Version 0.1.1-release-candidate.1.2\nNew notes", + want: { + "0.1.0-rc.1": "Notes", + "0.1.1-release-candidate.1.2": "New notes", + }, + }, + { + description: "should handle higher version number", + in: "Some random words\n## Version 10.1.0-rc.1\nNotes\n## Version 10.1.1-release-candidate.1.2\nNew notes", + want: { + "10.1.0-rc.1": "Notes", + "10.1.1-release-candidate.1.2": "New notes", + }, + }, + ]; + for (const testCase of testCases) { + it(testCase.description, () => { + const got = changelog.parseChangelog(testCase.in); + + expect(got).to.deep.equal(testCase.want); + }); + } + }); +}); diff --git a/src/extensions/change-log.ts b/src/extensions/change-log.ts new file mode 100644 index 00000000000..31112111b36 --- /dev/null +++ b/src/extensions/change-log.ts @@ -0,0 +1,127 @@ +import * as clc from "colorette"; +import { marked } from "marked"; +import * as path from "path"; +import * as semver from "semver"; +import * as TerminalRenderer from "marked-terminal"; +const Table = require("cli-table"); + +import { listExtensionVersions } from "./extensionsApi"; +import { readFile } from "./localHelper"; +import { logger } from "../logger"; +import * as refs from "./refs"; +import { logLabeledWarning } from "../utils"; + +marked.setOptions({ + renderer: new TerminalRenderer(), +}); + +const EXTENSIONS_CHANGELOG = "CHANGELOG.md"; +// Simplifed version of https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +const VERSION_LINE_REGEX = + /##.+?(\d+\.\d+\.\d+(?:-((\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?).*/; + +/* + * getReleaseNotesForUpdate fetches all version between toVersion and fromVersion and returns the relase notes + * for those versions if they exist. + * @param extensionRef + * @param fromVersion the version you are updating from + * @param toVersion the version you are upodating to + * @returns a Record of version number to releaseNotes for that version + */ +export async function getReleaseNotesForUpdate(args: { + extensionRef: string; + fromVersion: string; + toVersion: string; +}): Promise> { + const releaseNotes: Record = {}; + const filter = `id<="${args.toVersion}" AND id>"${args.fromVersion}"`; + const extensionVersions = await listExtensionVersions(args.extensionRef, filter); + extensionVersions.sort((ev1, ev2) => { + return -semver.compare(ev1.spec.version, ev2.spec.version); + }); + for (const extensionVersion of extensionVersions) { + if (extensionVersion.releaseNotes) { + const version = refs.parse(extensionVersion.ref).version!; + releaseNotes[version] = extensionVersion.releaseNotes; + } + } + return releaseNotes; +} + +/** + * displayReleaseNotes prints out a nicely formatted table containing all release notes in an update. + * If there is a major version change, it also prints a warning and highlights those release notes. + */ +export function displayReleaseNotes(releaseNotes: Record, fromVersion: string) { + const versions = [fromVersion].concat(Object.keys(releaseNotes)); + const breakingVersions = breakingChangesInUpdate(versions); + const table = new Table({ head: ["Version", "What's New"], style: { head: ["yellow", "bold"] } }); + for (const [version, note] of Object.entries(releaseNotes)) { + if (breakingVersions.includes(version)) { + table.push([clc.yellow(clc.bold(version)), marked(note)]); + } else { + table.push([version, marked(note)]); + } + } + + logger.info(clc.bold("What's new with this update:")); + if (breakingVersions.length) { + logLabeledWarning( + "warning", + "This is a major version update, which means it may contain breaking changes." + + " Read the release notes carefully before continuing with this update.", + ); + } + logger.info(table.toString()); +} + +/** + * breakingChangesInUpdate identifies which versions in an update are major changes. + * Exported for testing. + */ +export function breakingChangesInUpdate(versionsInUpdate: string[]): string[] { + const breakingVersions: string[] = []; + const semvers = versionsInUpdate.map((v) => semver.parse(v)!).sort(semver.compare); + for (let i = 1; i < semvers.length; i++) { + const hasMajorBump = semvers[i - 1].major < semvers[i].major; + const hasMinorBumpInPreview = + semvers[i - 1].major === 0 && + semvers[i].major === 0 && + semvers[i - 1].minor < semvers[i].minor; + if (hasMajorBump || hasMinorBumpInPreview) { + breakingVersions.push(semvers[i].raw); + } + } + return breakingVersions; +} + +/** + * getLocalChangelog checks directory for a CHANGELOG.md, and parses it into a map of + * version to release notes for that version. + * @param directory The directory to check for + * @returns + */ +export function getLocalChangelog(directory: string): Record { + const rawChangelog = readFile(path.resolve(directory, EXTENSIONS_CHANGELOG)); + return parseChangelog(rawChangelog); +} + +// Exported for testing. +export function parseChangelog(rawChangelog: string): Record { + const changelog: Record = {}; + let currentVersion = ""; + for (const line of rawChangelog.split("\n")) { + const matches = line.match(VERSION_LINE_REGEX); + if (matches) { + currentVersion = matches[1]; // The first capture group is the SemVer. + } else if (currentVersion) { + // Throw away lines that aren't under a specific version. + if (!changelog[currentVersion]) { + changelog[currentVersion] = line; + } else { + changelog[currentVersion] += `\n${line}`; + } + } + } + return changelog; +} diff --git a/src/extensions/changelog.ts b/src/extensions/changelog.ts deleted file mode 100644 index abd5da1117e..00000000000 --- a/src/extensions/changelog.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as clc from "cli-color"; -import * as marked from "marked"; -import * as path from "path"; -import * as semver from "semver"; -import TerminalRenderer = require("marked-terminal"); -import Table = require("cli-table"); - -import { listExtensionVersions } from "./extensionsApi"; -import { readFile } from "./localHelper"; -import { logger } from "../logger"; -import * as refs from "./refs"; -import { logLabeledWarning } from "../utils"; - -marked.setOptions({ - renderer: new TerminalRenderer(), -}); - -const EXTENSIONS_CHANGELOG = "CHANGELOG.md"; -const VERSION_LINE_REGEX = /##.*(\d+\.\d+\.\d+).*/; - -/* - * getReleaseNotesForUpdate fetches all version between toVersion and fromVersion and returns the relase notes - * for those versions if they exist. - * @param extensionRef - * @param fromVersion the version you are updating from - * @param toVersion the version you are upodating to - * @returns a Record of version number to releaseNotes for that version - */ -export async function getReleaseNotesForUpdate(args: { - extensionRef: string; - fromVersion: string; - toVersion: string; -}): Promise> { - const releaseNotes: Record = {}; - const filter = `id<="${args.toVersion}" AND id>"${args.fromVersion}"`; - const extensionVersions = await listExtensionVersions(args.extensionRef, filter); - extensionVersions.sort((ev1, ev2) => { - return -semver.compare(ev1.spec.version, ev2.spec.version); - }); - for (const extensionVersion of extensionVersions) { - if (extensionVersion.releaseNotes) { - const version = refs.parse(extensionVersion.ref).version!; - releaseNotes[version] = extensionVersion.releaseNotes; - } - } - return releaseNotes; -} - -/** - * displayReleaseNotes prints out a nicely formatted table containing all release notes in an update. - * If there is a major version change, it also prints a warning and highlights those release notes. - */ -export function displayReleaseNotes(releaseNotes: Record, fromVersion: string) { - const versions = [fromVersion].concat(Object.keys(releaseNotes)); - const breakingVersions = breakingChangesInUpdate(versions); - const table = new Table({ head: ["Version", "What's New"], style: { head: ["yellow", "bold"] } }); - for (const [version, note] of Object.entries(releaseNotes)) { - if (breakingVersions.includes(version)) { - table.push([clc.yellow.bold(version), marked(note)]); - } else { - table.push([version, marked(note)]); - } - } - - logger.info(clc.bold("What's new with this update:")); - if (breakingVersions.length) { - logLabeledWarning( - "warning", - "This is a major version update, which means it may contain breaking changes." + - " Read the release notes carefully before continuing with this update." - ); - } - logger.info(table.toString()); -} - -/** - * breakingChangesInUpdate identifies which versions in an update are major changes. - * Exported for testing. - */ -export function breakingChangesInUpdate(versionsInUpdate: string[]): string[] { - const breakingVersions: string[] = []; - const semvers = versionsInUpdate.map((v) => semver.parse(v)!).sort(semver.compare); - for (let i = 1; i < semvers.length; i++) { - const hasMajorBump = semvers[i - 1].major < semvers[i].major; - const hasMinorBumpInPreview = - semvers[i - 1].major == 0 && semvers[i].major == 0 && semvers[i - 1].minor < semvers[i].minor; - if (hasMajorBump || hasMinorBumpInPreview) { - breakingVersions.push(semvers[i].raw); - } - } - return breakingVersions; -} - -/** - * getLocalChangelog checks directory for a CHANGELOG.md, and parses it into a map of - * version to release notes for that version. - * @param directory The directory to check for - * @returns - */ -export function getLocalChangelog(directory: string): Record { - const rawChangelog = readFile(path.resolve(directory, EXTENSIONS_CHANGELOG)); - return parseChangelog(rawChangelog); -} - -// Exported for testing. -export function parseChangelog(rawChangelog: string): Record { - const changelog: Record = {}; - let currentVersion = ""; - for (const line of rawChangelog.split("\n")) { - const matches = line.match(VERSION_LINE_REGEX); - if (matches) { - currentVersion = matches[1]; // The first capture group is the SemVer. - } else if (currentVersion) { - // Throw away lines that aren't under a specific version. - if (!changelog[currentVersion]) { - changelog[currentVersion] = line; - } else { - changelog[currentVersion] += `\n${line}`; - } - } - } - return changelog; -} diff --git a/src/test/extensions/checkProjectBilling.spec.ts b/src/extensions/checkProjectBilling.spec.ts similarity index 94% rename from src/test/extensions/checkProjectBilling.spec.ts rename to src/extensions/checkProjectBilling.spec.ts index 425d30ea08d..e4e8814db81 100644 --- a/src/test/extensions/checkProjectBilling.spec.ts +++ b/src/extensions/checkProjectBilling.spec.ts @@ -2,9 +2,9 @@ import * as chai from "chai"; chai.use(require("chai-as-promised")); import * as sinon from "sinon"; -import * as checkProjectBilling from "../../extensions/checkProjectBilling"; -import * as prompt from "../../prompt"; -import * as cloudbilling from "../../gcp/cloudbilling"; +import * as checkProjectBilling from "./checkProjectBilling"; +import * as prompt from "../prompt"; +import * as cloudbilling from "../gcp/cloudbilling"; const expect = chai.expect; diff --git a/src/extensions/checkProjectBilling.ts b/src/extensions/checkProjectBilling.ts index 542aa1d52c6..66eac158ecd 100644 --- a/src/extensions/checkProjectBilling.ts +++ b/src/extensions/checkProjectBilling.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as opn from "open"; import * as cloudbilling from "../gcp/cloudbilling"; @@ -16,8 +16,8 @@ function logBillingStatus(enabled: boolean, projectId: string): void { if (!enabled) { throw new FirebaseError( `${logPrefix}: ${clc.bold( - projectId - )} could not be upgraded. Please add a billing account via the Firebase console before proceeding.` + projectId, + )} could not be upgraded. Please add a billing account via the Firebase console before proceeding.`, ); } utils.logLabeledSuccess(logPrefix, `${clc.bold(projectId)} has successfully been upgraded.`); @@ -30,7 +30,7 @@ async function openBillingAccount(projectId: string, url: string, open: boolean) if (open) { try { opn(url); - } catch (err) { + } catch (err: any) { logger.debug("Unable to open billing URL: " + err.stack); } } @@ -50,7 +50,7 @@ async function openBillingAccount(projectId: string, url: string, open: boolean) */ async function chooseBillingAccount( projectId: string, - accounts: cloudbilling.BillingAccount[] + accounts: cloudbilling.BillingAccount[], ): Promise { const choices = accounts.map((m) => m.displayName); choices.push(ADD_BILLING_ACCOUNT); @@ -68,7 +68,7 @@ Please select the one that you would like to associate with this project:`, const billingURL = `https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`; billingEnabled = await openBillingAccount(projectId, billingURL, true); } else { - const billingAccount = accounts.find((a) => a.displayName == answer); + const billingAccount = accounts.find((a) => a.displayName === answer); billingEnabled = await cloudbilling.setBillingAccount(projectId, billingAccount!.name); } @@ -84,10 +84,10 @@ async function setUpBillingAccount(projectId: string) { logger.info(); logger.info( - `Extension require your project to be upgraded to the Blaze plan. Please visit the following link to add a billing account:` + `Extension require your project to be upgraded to the Blaze plan. Please visit the following link to add a billing account:`, ); logger.info(); - logger.info(clc.bold.underline(billingURL)); + logger.info(clc.bold(clc.underline(billingURL))); logger.info(); const open = await prompt.promptOnce({ diff --git a/src/extensions/diagnose.spec.ts b/src/extensions/diagnose.spec.ts new file mode 100644 index 00000000000..c2203833a11 --- /dev/null +++ b/src/extensions/diagnose.spec.ts @@ -0,0 +1,96 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as resourceManager from "../gcp/resourceManager"; +import * as pn from "../getProjectNumber"; +import * as diagnose from "./diagnose"; +import * as extensionsApi from "./extensionsApi"; +import * as prompt from "../prompt"; + +const GOOD_BINDING = { + role: "roles/firebasemods.serviceAgent", + members: ["serviceAccount:service-123456@gcp-sa-firebasemods.iam.gserviceaccount.com"], +}; + +describe("diagnose", () => { + let getIamStub: sinon.SinonStub; + let setIamStub: sinon.SinonStub; + let getProjectNumberStub: sinon.SinonStub; + let promptOnceStub: sinon.SinonStub; + let listInstancesStub: sinon.SinonStub; + + beforeEach(() => { + getIamStub = sinon + .stub(resourceManager, "getIamPolicy") + .throws("unexpected call to resourceManager.getIamStub"); + setIamStub = sinon + .stub(resourceManager, "setIamPolicy") + .throws("unexpected call to resourceManager.setIamPolicy"); + getProjectNumberStub = sinon + .stub(pn, "getProjectNumber") + .throws("unexpected call to pn.getProjectNumber"); + promptOnceStub = sinon + .stub(prompt, "promptOnce") + .throws("unexpected call to prompt.promptOnce"); + listInstancesStub = sinon + .stub(extensionsApi, "listInstances") + .throws("unexpected call to extensionsApi.listInstances"); + + getProjectNumberStub.resolves(123456); + listInstancesStub.resolves([]); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("should succeed when IAM policy is correct (no fix)", async () => { + getIamStub.resolves({ + etag: "etag", + version: 3, + bindings: [GOOD_BINDING], + }); + promptOnceStub.resolves(false); + + expect(await diagnose.diagnose("project_id")).to.be.true; + + expect(getIamStub).to.have.been.calledWith("project_id"); + expect(setIamStub).to.not.have.been.called; + }); + + it("should fail when project IAM policy missing extensions service agent (no fix)", async () => { + getIamStub.resolves({ + etag: "etag", + version: 3, + bindings: [], + }); + promptOnceStub.resolves(false); + + expect(await diagnose.diagnose("project_id")).to.be.false; + + expect(getIamStub).to.have.been.calledWith("project_id"); + expect(setIamStub).to.not.have.been.called; + }); + + it("should fix the project IAM policy by adding missing bindings", async () => { + getIamStub.resolves({ + etag: "etag", + version: 3, + bindings: [], + }); + setIamStub.resolves(); + promptOnceStub.resolves(true); + + expect(await diagnose.diagnose("project_id")).to.be.true; + + expect(getIamStub).to.have.been.calledWith("project_id"); + expect(setIamStub).to.have.been.calledWith( + "project_id", + { + etag: "etag", + version: 3, + bindings: [GOOD_BINDING], + }, + "bindings", + ); + }); +}); diff --git a/src/extensions/diagnose.ts b/src/extensions/diagnose.ts new file mode 100644 index 00000000000..b0a36a3c7a9 --- /dev/null +++ b/src/extensions/diagnose.ts @@ -0,0 +1,74 @@ +import { logPrefix } from "./extensionsHelper"; +import { getProjectNumber } from "../getProjectNumber"; +import * as utils from "../utils"; +import * as resourceManager from "../gcp/resourceManager"; +import { listInstances } from "./extensionsApi"; +import { promptOnce } from "../prompt"; +import { logger } from "../logger"; +import { FirebaseError } from "../error"; + +const SERVICE_AGENT_ROLE = "roles/firebasemods.serviceAgent"; + +/** + * Diagnoses and optionally fixes known issues with project configuration, ex. missing Extensions Service Agent permissions. + * @param projectId ID of the project we're querying + */ +export async function diagnose(projectId: string): Promise { + const projectNumber = await getProjectNumber({ projectId }); + const firexSaProjectId = utils.envOverride( + "FIREBASE_EXTENSIONS_SA_PROJECT_ID", + "gcp-sa-firebasemods", + ); + + const saEmail = `service-${projectNumber}@${firexSaProjectId}.iam.gserviceaccount.com`; + + utils.logLabeledBullet(logPrefix, "Checking project IAM policy..."); + + // Call ListExtensionInstances to make sure Extensions Service Agent is provisioned. + await listInstances(projectId); + + let policy; + try { + policy = await resourceManager.getIamPolicy(projectId); + logger.debug(policy); + } catch (e) { + if (e instanceof FirebaseError && e.status === 403) { + throw new FirebaseError( + "Unable to get project IAM policy, permission denied (403). Please " + + "make sure you have sufficient project privileges or if this is a brand new project " + + "try again in a few minutes.", + ); + } + throw e; + } + + if ( + policy.bindings.find( + (b) => b.role === SERVICE_AGENT_ROLE && b.members.includes("serviceAccount:" + saEmail), + ) + ) { + utils.logLabeledSuccess(logPrefix, "Project IAM policy OK"); + return true; + } else { + utils.logWarning( + "Firebase Extensions Service Agent is missing a required IAM role " + + "`Firebase Extensions API Service Agent`.", + ); + const fix = await promptOnce({ + type: "confirm", + message: + "Would you like to fix the issue by updating IAM policy to include Firebase " + + "Extensions Service Agent with role `Firebase Extensions API Service Agent`", + }); + if (fix) { + policy.bindings.push({ + role: SERVICE_AGENT_ROLE, + members: ["serviceAccount:" + saEmail], + }); + await resourceManager.setIamPolicy(projectId, policy, "bindings"); + utils.logSuccess("Project IAM policy updated successfully"); + return true; + } + return false; + } +} diff --git a/src/extensions/displayExtensionInfo.spec.ts b/src/extensions/displayExtensionInfo.spec.ts new file mode 100644 index 00000000000..aaa200e691f --- /dev/null +++ b/src/extensions/displayExtensionInfo.spec.ts @@ -0,0 +1,148 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as iam from "../gcp/iam"; +import * as displayExtensionInfo from "./displayExtensionInfo"; +import { ExtensionSpec, ExtensionVersion, Resource } from "./types"; +import { ParamType } from "./types"; + +const SPEC: ExtensionSpec = { + name: "test", + displayName: "My Extension", + description: "My extension's description", + version: "1.0.0", + license: "MIT", + apis: [ + { apiName: "api1.googleapis.com", reason: "" }, + { apiName: "api2.googleapis.com", reason: "" }, + ], + roles: [ + { role: "role1", reason: "" }, + { role: "role2", reason: "" }, + ], + resources: [ + { name: "resource1", type: "firebaseextensions.v1beta.function", description: "desc" }, + { name: "resource2", type: "other", description: "" } as unknown as Resource, + { + name: "taskResource", + type: "firebaseextensions.v1beta.function", + properties: { + taskQueueTrigger: {}, + }, + }, + ], + author: { authorName: "Tester", url: "firebase.google.com" }, + contributors: [{ authorName: "Tester 2" }], + billingRequired: true, + sourceUrl: "test.com", + params: [ + { + param: "secret", + label: "Secret", + type: ParamType.SECRET, + }, + ], + systemParams: [], + events: [ + { + type: "abc.def.my-event", + description: "desc", + }, + ], + lifecycleEvents: [ + { + stage: "ON_INSTALL", + taskQueueTriggerFunction: "taskResource", + }, + ], +}; + +const EXT_VERSION: ExtensionVersion = { + name: "publishers/pub/extensions/my-ext/versions/1.0.0", + ref: "pub/my-ext@1.0.0", + state: "PUBLISHED", + spec: SPEC, + hash: "abc123", + sourceDownloadUri: "https://google.com", + buildSourceUri: "https://github.com/pub/extensions/my-ext", + listing: { + state: "APPROVED", + }, +}; + +describe("displayExtensionInfo", () => { + describe("displayExtInfo", () => { + let getRoleStub: sinon.SinonStub; + beforeEach(() => { + getRoleStub = sinon.stub(iam, "getRole"); + getRoleStub.withArgs("role1").resolves({ + title: "Role 1", + description: "a role", + }); + getRoleStub.withArgs("role2").resolves({ + title: "Role 2", + description: "a role", + }); + getRoleStub.withArgs("cloudtasks.enqueuer").resolves({ + title: "Cloud Task Enqueuer", + description: "Enqueue tasks", + }); + getRoleStub.withArgs("secretmanager.secretAccessor").resolves({ + title: "Secret Accessor", + description: "Access Secrets", + }); + }); + + afterEach(() => { + getRoleStub.restore(); + }); + + it("should display info during install", async () => { + const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo({ spec: SPEC }); + expect(loggedLines[0]).to.include(SPEC.displayName); + expect(loggedLines[1]).to.include(SPEC.description); + expect(loggedLines[2]).to.include(SPEC.version); + expect(loggedLines[3]).to.include(SPEC.license); + expect(loggedLines[4]).to.include("resource1 (Cloud Function (1st gen))"); + expect(loggedLines[4]).to.include("resource2 (other)"); + expect(loggedLines[4]).to.include("taskResource (Cloud Function (1st gen))"); + expect(loggedLines[4]).to.include("taskResource (Cloud Task queue)"); + expect(loggedLines[4]).to.include("secret (Cloud Secret Manager secret)"); + expect(loggedLines[5]).to.include("abc.def.my-event"); + expect(loggedLines[6]).to.include("api1.googleapis.com"); + expect(loggedLines[6]).to.include("api1.googleapis.com"); + expect(loggedLines[6]).to.include("cloudtasks.googleapis.com"); + expect(loggedLines[7]).to.include("Role 1"); + expect(loggedLines[7]).to.include("Role 2"); + expect(loggedLines[7]).to.include("Cloud Task Enqueuer"); + }); + + it("should display additional information for a published extension", async () => { + const loggedLines = await displayExtensionInfo.displayExtensionVersionInfo({ + spec: SPEC, + extensionVersion: EXT_VERSION, + latestApprovedVersion: "1.0.0", + latestVersion: "1.0.0", + }); + expect(loggedLines[0]).to.include(SPEC.displayName); + expect(loggedLines[1]).to.include(SPEC.description); + expect(loggedLines[2]).to.include(SPEC.version); + expect(loggedLines[3]).to.include("Accepted"); + expect(loggedLines[4]).to.include("View in Extensions Hub"); + expect(loggedLines[5]).to.include(EXT_VERSION.buildSourceUri); + expect(loggedLines[6]).to.include(SPEC.license); + expect(loggedLines[7]).to.include("resource1 (Cloud Function (1st gen))"); + expect(loggedLines[7]).to.include("resource2 (other)"); + expect(loggedLines[7]).to.include("taskResource (Cloud Function (1st gen))"); + expect(loggedLines[7]).to.include("taskResource (Cloud Task queue)"); + expect(loggedLines[7]).to.include("secret (Cloud Secret Manager secret)"); + expect(loggedLines[8]).to.include("abc.def.my-event"); + expect(loggedLines[9]).to.include("api1.googleapis.com"); + expect(loggedLines[9]).to.include("api1.googleapis.com"); + expect(loggedLines[9]).to.include("cloudtasks.googleapis.com"); + expect(loggedLines[10]).to.include("Role 1"); + expect(loggedLines[10]).to.include("Role 2"); + expect(loggedLines[10]).to.include("Cloud Task Enqueuer"); + }); + }); +}); diff --git a/src/extensions/displayExtensionInfo.ts b/src/extensions/displayExtensionInfo.ts index ca7e14d82e7..e31d199d960 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -1,267 +1,229 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as marked from "marked"; -import TerminalRenderer = require("marked-terminal"); +import * as clc from "colorette"; +import { marked } from "marked"; +import * as semver from "semver"; +import * as TerminalRenderer from "marked-terminal"; +import * as path from "path"; -import * as extensionsApi from "./extensionsApi"; -import * as utils from "../utils"; -import { confirm, logPrefix } from "./extensionsHelper"; +import * as refs from "../extensions/refs"; import { logger } from "../logger"; -import { FirebaseError } from "../error"; -import { promptOnce } from "../prompt"; +import { + Api, + ExtensionSpec, + ExtensionVersion, + LifecycleEvent, + ExternalService, + Role, + Param, + Resource, + FUNCTIONS_RESOURCE_TYPE, + EventDescriptor, +} from "./types"; +import * as iam from "../gcp/iam"; +import { SECRET_ROLE, usesSecrets } from "./secretsUtils"; marked.setOptions({ renderer: new TerminalRenderer(), }); -const additionColor = clc.green; -const deletionColor = clc.red; +const TASKS_ROLE = "cloudtasks.enqueuer"; +const TASKS_API = "cloudtasks.googleapis.com"; /** - * displayExtInfo prints the extension info displayed when running ext:install. + * Displays info about an extension version, whether it is uploaded to the registry or a local spec. * - * @param extensionName name of the extension to display information about - * @param spec extension spec - * @param published whether or not the extension is a published extension - */ -export function displayExtInfo( - extensionName: string, - publisher: string, - spec: extensionsApi.ExtensionSpec, - published = false -): string[] { - const lines = []; - lines.push(`**Name**: ${spec.displayName}`); - if (publisher) { - lines.push(`**Publisher**: ${publisher}`); - } + * @param spec the extension spec + * @param extensionVersion the extension version + * */ +export async function displayExtensionVersionInfo(args: { + spec: ExtensionSpec; + extensionVersion?: ExtensionVersion; + latestApprovedVersion?: string; + latestVersion?: string; +}): Promise { + const { spec, extensionVersion, latestApprovedVersion, latestVersion } = args; + const lines: string[] = []; + const extensionRef = extensionVersion + ? refs.toExtensionRef(refs.parse(extensionVersion?.ref)) + : ""; + lines.push( + `${clc.bold("Extension:")} ${spec.displayName ?? "Unnamed extension"} ${ + extensionRef ? `(${extensionRef})` : "" + }`, + ); if (spec.description) { - lines.push(`**Description**: ${spec.description}`); + lines.push(`${clc.bold("Description:")} ${spec.description}`); } - if (published) { - if (spec.license) { - lines.push(`**License**: ${spec.license}`); - } - lines.push(`**Source code**: ${spec.sourceUrl}`); + let versionNote = ""; + const latestRelevantVersion = latestApprovedVersion || latestVersion; + if (latestRelevantVersion && semver.eq(spec.version, latestRelevantVersion)) { + versionNote = `- ${clc.green("Latest")}`; } - if (lines.length > 0) { - utils.logLabeledBullet(logPrefix, `information about '${clc.bold(extensionName)}':`); - const infoStr = lines.join("\n"); - // Convert to markdown and convert any trailing newlines to a single newline. - const formatted = marked(infoStr).replace(/\n+$/, "\n"); - logger.info(formatted); - // Return for testing purposes. - return lines; - } else { - throw new FirebaseError( - "Error occurred during installation: cannot parse info from source spec", - { - context: { - spec: spec, - extensionName: extensionName, - }, - } - ); + if (extensionVersion?.state === "DEPRECATED") { + versionNote = `- ${clc.red("Deprecated")}`; } -} - -/** - * Prints out all changes to the spec that don't require explicit approval or input. - * - * @param spec The current spec of a ExtensionInstance. - * @param newSpec The spec that the ExtensionInstance is being updated to - * @param published whether or not this spec is for a published extension - */ -export function displayUpdateChangesNoInput( - spec: extensionsApi.ExtensionSpec, - newSpec: extensionsApi.ExtensionSpec -): string[] { - const lines: string[] = []; - if (spec.displayName !== newSpec.displayName) { - lines.push( - "", - "**Name:**", - deletionColor(`- ${spec.displayName}`), - additionColor(`+ ${newSpec.displayName}`) - ); + lines.push(`${clc.bold("Version:")} ${spec.version} ${versionNote}`); + if (extensionVersion) { + let reviewStatus: string; + switch (extensionVersion.listing?.state) { + case "APPROVED": + reviewStatus = clc.bold(clc.green("Accepted")); + break; + case "REJECTED": + reviewStatus = clc.bold(clc.red("Rejected")); + break; + default: + reviewStatus = clc.bold(clc.yellow("Unreviewed")); + } + lines.push(`${clc.bold("Review status:")} ${reviewStatus}`); + if (latestApprovedVersion) { + lines.push( + `${clc.bold("View in Extensions Hub:")} https://extensions.dev/extensions/${extensionRef}`, + ); + } + if (extensionVersion.buildSourceUri) { + const buildSourceUri = new URL(extensionVersion.buildSourceUri!); + buildSourceUri.pathname = path.join( + buildSourceUri.pathname, + extensionVersion.extensionRoot ?? "", + ); + lines.push(`${clc.bold("Source in GitHub:")} ${buildSourceUri}`); + } else { + lines.push( + `${clc.bold("Source download URI:")} ${extensionVersion.sourceDownloadUri ?? "-"}`, + ); + } } - - if (spec.author?.authorName !== newSpec.author?.authorName) { - lines.push( - "", - "**Author:**", - deletionColor(`- ${spec.author?.authorName}`), - additionColor(`+ ${spec.author?.authorName}`) - ); + lines.push(`${clc.bold("License:")} ${spec.license ?? "-"}`); + lines.push(displayResources(spec)); + if (spec.events?.length) { + lines.push(displayEvents(spec)); } - - if (spec.description !== newSpec.description) { - lines.push( - "", - "**Description:**", - deletionColor(`- ${spec.description}`), - additionColor(`+ ${newSpec.description}`) - ); + if (spec.externalServices?.length) { + lines.push(displayExternalServices(spec)); } - - if (spec.sourceUrl !== newSpec.sourceUrl) { - lines.push( - "", - "**Source code:**", - deletionColor(`- ${spec.sourceUrl}`), - additionColor(`+ ${newSpec.sourceUrl}`) - ); + const apis = impliedApis(spec); + if (apis.length) { + lines.push(displayApis(apis)); } - - if (spec.billingRequired && !newSpec.billingRequired) { - lines.push("", "**Billing is no longer required for this extension.**"); + const roles = impliedRoles(spec); + if (roles.length) { + lines.push(await displayRoles(roles)); } - logger.info(marked(lines.join("\n"))); + logger.info(`\n${lines.join("\n")}`); return lines; } -/** - * Checks for spec changes that require explicit user consent, - * and individually prompts the user for each changed field. - * - * @param spec The current spec of a ExtensionInstance - * @param newSpec The spec that the ExtensionInstance is being updated to - */ -export async function displayUpdateChangesRequiringConfirmation(args: { - spec: extensionsApi.ExtensionSpec; - newSpec: extensionsApi.ExtensionSpec; - nonInteractive: boolean; - force: boolean; -}): Promise { - const equals = (a: any, b: any) => { - return _.isEqual(a, b); - }; - if (args.spec.license !== args.newSpec.license) { - const message = - "\n" + - "**License**\n" + - deletionColor(args.spec.license ? `- ${args.spec.license}\n` : "- None\n") + - additionColor(args.newSpec.license ? `+ ${args.newSpec.license}\n` : "+ None\n"); - logger.info(message); - if ( - !(await confirm({ nonInteractive: args.nonInteractive, force: args.force, default: true })) - ) { - throw new FirebaseError( - "Unable to update this extension instance without explicit consent for the change to 'License'." - ); +export function displayExternalServices(spec: ExtensionSpec) { + const lines = + spec.externalServices?.map((service: ExternalService) => { + return ` - ${clc.cyan(`${service.name} (${service.pricingUri})`)}`; + }) ?? []; + return clc.bold("External services used:\n") + lines.join("\n"); +} + +export function displayEvents(spec: ExtensionSpec) { + const lines = + spec.events?.map((event: EventDescriptor) => { + return ` - ${clc.magenta(event.type)}${event.description ? `: ${event.description}` : ""}`; + }) ?? []; + return clc.bold("Events emitted:\n") + lines.join("\n"); +} + +export function displayResources(spec: ExtensionSpec) { + const lines = spec.resources.map((resource: Resource) => { + let type: string = resource.type; + switch (resource.type) { + case "firebaseextensions.v1beta.function": + type = "Cloud Function (1st gen)"; + break; + case "firebaseextensions.v1beta.v2function": + type = "Cloud Function (2nd gen)"; + break; + default: } - } - const apisDiffDeletions = _.differenceWith( - args.spec.apis, - _.get(args.newSpec, "apis", []), - equals + return ` - ${clc.blue(`${resource.name} (${type})`)}${ + resource.description ? `: ${resource.description}` : "" + }`; + }); + lines.push( + ...new Set( + spec.lifecycleEvents?.map((event: LifecycleEvent) => { + return ` - ${clc.blue(`${event.taskQueueTriggerFunction} (Cloud Task queue)`)}`; + }), + ), ); - const apisDiffAdditions = _.differenceWith( - args.newSpec.apis, - _.get(args.spec, "apis", []), - equals + lines.push( + ...spec.params + .filter((param: Param) => { + return param.type === "SECRET"; + }) + .map((param: Param) => { + return ` - ${clc.blue(`${param.param} (Cloud Secret Manager secret)`)}`; + }), ); - if (apisDiffDeletions.length || apisDiffAdditions.length) { - let message = "\n**APIs:**\n"; - apisDiffDeletions.forEach((api) => { - message += deletionColor(`- ${api.apiName} (${api.reason})\n`); - }); - apisDiffAdditions.forEach((api) => { - message += additionColor(`+ ${api.apiName} (${api.reason})\n`); - }); - logger.info(message); - if ( - !(await confirm({ nonInteractive: args.nonInteractive, force: args.force, default: true })) - ) { - throw new FirebaseError( - "Unable to update this extension instance without explicit consent for the change to 'APIs'." - ); - } - } + return clc.bold("Resources created:\n") + (lines.length ? lines.join("\n") : " - None"); +} - const resourcesDiffDeletions = _.differenceWith( - args.spec.resources, - _.get(args.newSpec, "resources", []), - compareResources - ); - const resourcesDiffAdditions = _.differenceWith( - args.newSpec.resources, - _.get(args.spec, "resources", []), - compareResources - ); - if (resourcesDiffDeletions.length || resourcesDiffAdditions.length) { - let message = "\n**Resources:**\n"; - resourcesDiffDeletions.forEach((resource) => { - message += deletionColor(` - ${getResourceReadableName(resource)}`); - }); - resourcesDiffAdditions.forEach((resource) => { - message += additionColor(`+ ${getResourceReadableName(resource)}`); - }); - logger.info(message); - if ( - !(await confirm({ nonInteractive: args.nonInteractive, force: args.force, default: true })) - ) { - throw new FirebaseError( - "Unable to update this extension instance without explicit consent for the change to 'Resources'." - ); - } - } +/** + * Returns a string representing a Role, see + * https://cloud.google.com/iam/reference/rest/v1/organizations.roles#Role + * for more details on parameters of a Role. + * @param role to get info for + * @return {string} string representation for role + */ +export async function retrieveRoleInfo(role: string) { + const res = await iam.getRole(role); + return ` - ${clc.yellow(res.title!)}${res.description ? `: ${res.description}` : ""}`; +} - const rolesDiffDeletions = _.differenceWith( - args.spec.roles, - _.get(args.newSpec, "roles", []), - equals +async function displayRoles(roles: Role[]): Promise { + const lines: string[] = await Promise.all( + roles.map((role: Role) => { + return retrieveRoleInfo(role.role); + }), ); - const rolesDiffAdditions = _.differenceWith( - args.newSpec.roles, - _.get(args.spec, "roles", []), - equals + return clc.bold("Roles granted:\n") + lines.join("\n"); +} + +function displayApis(apis: Api[]): string { + const lines: string[] = apis.map((api: Api) => { + return ` - ${clc.cyan(api.apiName!)}: ${api.reason}`; + }); + return clc.bold("APIs used:\n") + lines.join("\n"); +} + +function usesTasks(spec: ExtensionSpec): boolean { + return spec.resources.some( + (r: Resource) => + r.type === FUNCTIONS_RESOURCE_TYPE && r.properties?.taskQueueTrigger !== undefined, ); +} - if (rolesDiffDeletions.length || rolesDiffAdditions.length) { - let message = "\n**Permissions:**\n"; - rolesDiffDeletions.forEach((role) => { - message += deletionColor(`- ${role.role} (${role.reason})\n`); +function impliedRoles(spec: ExtensionSpec): Role[] { + const roles: Role[] = []; + if (usesSecrets(spec) && !spec.roles?.some((r: Role) => r.role === SECRET_ROLE)) { + roles.push({ + role: SECRET_ROLE, + reason: "Allows the extension to read secret values from Cloud Secret Manager.", }); - rolesDiffAdditions.forEach((role) => { - message += additionColor(`+ ${role.role} (${role.reason})\n`); - }); - logger.info(message); - if ( - !(await confirm({ nonInteractive: args.nonInteractive, force: args.force, default: true })) - ) { - throw new FirebaseError( - "Unable to update this extension instance without explicit consent for the change to 'Permissions'." - ); - } } - - if (!args.spec.billingRequired && args.newSpec.billingRequired) { - logger.info("Billing is now required for the new version of this extension."); - if ( - !(await confirm({ nonInteractive: args.nonInteractive, force: args.force, default: true })) - ) { - throw new FirebaseError( - "Unable to update this extension instance without explicit consent for the change to 'BillingRequired'." - ); - } + if (usesTasks(spec) && !spec.roles?.some((r: Role) => r.role === TASKS_ROLE)) { + roles.push({ + role: TASKS_ROLE, + reason: "Allows the extension to enqueue Cloud Tasks.", + }); } + return roles.concat(spec.roles ?? []); } -function compareResources(resource1: extensionsApi.Resource, resource2: extensionsApi.Resource) { - return resource1.name == resource2.name && resource1.type == resource2.type; -} - -function getResourceReadableName(resource: extensionsApi.Resource): string { - return resource.type === "firebaseextensions.v1beta.function" - ? `${resource.name} (Cloud Function): ${resource.description}\n` - : `${resource.name} (${resource.type})\n`; -} +function impliedApis(spec: ExtensionSpec): Api[] { + const apis: Api[] = []; + if (usesTasks(spec) && !spec.apis?.some((a: Api) => a.apiName === TASKS_API)) { + apis.push({ + apiName: TASKS_API, + reason: "Allows the extension to enqueue Cloud Tasks.", + }); + } -/** - * Prints a clickable link where users can download the source code for an Extension Version. - */ -export function printSourceDownloadLink(sourceDownloadUri: string): void { - const sourceDownloadMsg = `Want to review the source code that will be installed? Download it here: ${sourceDownloadUri}`; - utils.logBullet(marked(sourceDownloadMsg)); + return apis.concat(spec.apis ?? []); } diff --git a/src/extensions/emulator/optionsHelper.spec.ts b/src/extensions/emulator/optionsHelper.spec.ts new file mode 100644 index 00000000000..90d6ab4704b --- /dev/null +++ b/src/extensions/emulator/optionsHelper.spec.ts @@ -0,0 +1,178 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as optionsHelper from "./optionsHelper"; +import { ExtensionSpec, Param, ParamType } from "../types"; +import * as paramHelper from "../paramHelper"; + +describe("optionsHelper", () => { + describe("getParams", () => { + const testOptions = { + project: "test", + testParams: "test.env", + }; + const autoParams = { + PROJECT_ID: "test", + EXT_INSTANCE_ID: "test", + DATABASE_INSTANCE: "test", + DATABASE_URL: "https://test.firebaseio.com", + STORAGE_BUCKET: "test.appspot.com", + }; + let testSpec: ExtensionSpec; + let readEnvFileStub: sinon.SinonStub; + + beforeEach(() => { + testSpec = { + name: "test", + version: "0.1.0", + resources: [], + sourceUrl: "https://my.stuff.com", + params: [], + systemParams: [], + }; + readEnvFileStub = sinon.stub(paramHelper, "readEnvFile"); + }); + + afterEach(() => { + readEnvFileStub.restore(); + }); + + it("should return user and autopopulated params", () => { + testSpec.params = [ + { + label: "param1", + param: "USER_PARAM1", + }, + { + label: "param2", + param: "USER_PARAM2", + }, + ]; + readEnvFileStub.returns({ + USER_PARAM1: "val1", + USER_PARAM2: "val2", + }); + + expect(optionsHelper.getParams(testOptions, testSpec)).to.deep.eq({ + ...{ + USER_PARAM1: "val1", + USER_PARAM2: "val2", + }, + ...autoParams, + }); + }); + + it("should subsitute into params that reference other params", () => { + testSpec.params = [ + { + label: "param1", + param: "USER_PARAM1", + }, + { + label: "param2", + param: "USER_PARAM2", + }, + { + label: "param3", + param: "USER_PARAM3", + }, + ]; + readEnvFileStub.returns({ + USER_PARAM1: "${PROJECT_ID}-hello", + USER_PARAM2: "val2", + USER_PARAM3: "${USER_PARAM2}", + }); + + expect(optionsHelper.getParams(testOptions, testSpec)).to.deep.eq({ + ...{ + USER_PARAM1: "test-hello", + USER_PARAM2: "val2", + USER_PARAM3: "val2", + }, + ...autoParams, + }); + }); + + it("should fallback to defaults if a value isn't provided", () => { + testSpec.params = [ + { + label: "param1", + param: "USER_PARAM1", + default: "hi", + required: true, + }, + { + label: "param2", + param: "USER_PARAM2", + default: "hello", + required: true, + }, + ]; + readEnvFileStub.returns({}); + + expect(optionsHelper.getParams(testOptions, testSpec)).to.deep.eq({ + ...{ + USER_PARAM1: "hi", + USER_PARAM2: "hello", + }, + ...autoParams, + }); + }); + }); + + const TEST_SELECT_PARAM: Param = { + param: "SELECT_PARAM", + label: "A select param", + type: ParamType.SELECT, + }; + const TEST_STRING_PARAM: Param = { + param: "STRING_PARAM", + label: "A string param", + type: ParamType.STRING, + }; + const TEST_MULTISELECT_PARAM: Param = { + param: "MULTISELECT_PARAM", + label: "A multiselect param", + type: ParamType.MULTISELECT, + }; + const TEST_SECRET_PARAM: Param = { + param: "SECRET_PARAM", + label: "A secret param", + type: ParamType.SECRET, + }; + const TEST_PARAMS: Param[] = [ + TEST_SELECT_PARAM, + TEST_STRING_PARAM, + TEST_MULTISELECT_PARAM, + TEST_SECRET_PARAM, + ]; + const TEST_PARAM_VALUES = { + SELECT_PARAM: "select", + STRING_PARAM: "string", + MULTISELECT_PARAM: "multiselect", + SECRET_PARAM: "projects/test/secrets/mysecret/versionms/latest", + }; + + describe("getNonSecretEnv", () => { + it("should return only params that are not secret", () => { + expect(optionsHelper.getNonSecretEnv(TEST_PARAMS, TEST_PARAM_VALUES)).to.deep.equal({ + SELECT_PARAM: "select", + STRING_PARAM: "string", + MULTISELECT_PARAM: "multiselect", + }); + }); + }); + + describe("getSecretEnv", () => { + it("should return only params that are secret", () => { + expect(optionsHelper.getSecretEnvVars(TEST_PARAMS, TEST_PARAM_VALUES)).to.have.deep.members([ + { + projectId: "test", + key: "SECRET_PARAM", + secret: "mysecret", + version: "latest", + }, + ]); + }); + }); +}); diff --git a/src/extensions/emulator/optionsHelper.ts b/src/extensions/emulator/optionsHelper.ts index 19fe0454560..aca084c904d 100644 --- a/src/extensions/emulator/optionsHelper.ts +++ b/src/extensions/emulator/optionsHelper.ts @@ -1,49 +1,95 @@ -import * as fs from "fs-extra"; -import * as _ from "lodash"; import { ParsedTriggerDefinition } from "../../emulator/functionsEmulatorShared"; -import * as path from "path"; import * as paramHelper from "../paramHelper"; import * as specHelper from "./specHelper"; -import * as localHelper from "../localHelper"; import * as triggerHelper from "./triggerHelper"; -import { ExtensionSpec, Resource } from "../extensionsApi"; +import { ExtensionSpec, Param, ParamType } from "../types"; import * as extensionsHelper from "../extensionsHelper"; -import { Config } from "../../config"; -import { FirebaseError } from "../../error"; -import { EmulatorLogger } from "../../emulator/emulatorLogger"; +import * as planner from "../../deploy/extensions/planner"; import { needProjectId } from "../../projectUtils"; -import { Emulators } from "../../emulator/types"; +import { SecretEnvVar } from "../../deploy/functions/backend"; +import { Runtime } from "../../deploy/functions/runtimes/supported"; -export async function buildOptions(options: any): Promise { - const extensionDir = localHelper.findExtensionYaml(process.cwd()); - options.extensionDir = extensionDir; - const spec = await specHelper.readExtensionYaml(extensionDir); - extensionsHelper.validateSpec(spec); +/** + * TODO: Better name? Also, should this be in extensionsEmulator instead? + */ +export async function getExtensionFunctionInfo( + instance: planner.DeploymentInstanceSpec, + paramValues: Record, +): Promise<{ + runtime: Runtime; + extensionTriggers: ParsedTriggerDefinition[]; + nonSecretEnv: Record; + secretEnvVariables: SecretEnvVar[]; +}> { + const spec = await planner.getExtensionSpec(instance); + const functionResources = specHelper.getFunctionResourcesWithParamSubstitution(spec, paramValues); + const extensionTriggers: ParsedTriggerDefinition[] = functionResources + .map((r) => triggerHelper.functionResourceToEmulatedTriggerDefintion(r, instance.systemParams)) + .map((trigger) => { + trigger.name = `ext-${instance.instanceId}-${trigger.name}`; + return trigger; + }); + const runtime = specHelper.getRuntime(functionResources); + + const nonSecretEnv = getNonSecretEnv(spec.params ?? [], paramValues); + const secretEnvVariables = getSecretEnvVars(spec.params ?? [], paramValues); + return { + extensionTriggers, + runtime, + nonSecretEnv, + secretEnvVariables, + }; +} - const params = getParams(options, spec); +const isSecretParam = (p: Param) => + p.type === extensionsHelper.SpecParamType.SECRET || p.type === ParamType.SECRET; - extensionsHelper.validateCommandLineParams(params, spec.params); +/** + * getNonSecretEnv checks extension spec for secret params, and returns env without those secret params + * @param params A list of params to check for secret params + * @param paramValues A Record of all params to their values + */ +export function getNonSecretEnv( + params: Param[], + paramValues: Record, +): Record { + const getNonSecretEnv: Record = Object.assign({}, paramValues); + const secretParams = params.filter(isSecretParam); + for (const p of secretParams) { + delete getNonSecretEnv[p.param]; + } + return getNonSecretEnv; +} - const functionResources = specHelper.getFunctionResourcesWithParamSubstitution( - spec, - params - ) as Resource[]; - let testConfig; - if (options.testConfig) { - testConfig = readTestConfigFile(options.testConfig); - checkTestConfig(testConfig, functionResources); +/** + * getSecretEnvVars checks which params are secret, and returns a list of SecretEnvVar for each one that is is in use + * @param params A list of params to check for secret params + * @param paramValues A Record of all params to their values + */ +export function getSecretEnvVars( + params: Param[], + paramValues: Record, +): SecretEnvVar[] { + const secretEnvVar: SecretEnvVar[] = []; + const secretParams = params.filter(isSecretParam); + for (const s of secretParams) { + if (paramValues[s.param]) { + const [, projectId, , secret, , version] = paramValues[s.param].split("/"); + secretEnvVar.push({ + key: s.param, + secret, + projectId, + version, + }); + } + // TODO: Throw an error if a required secret is missing? } - options.config = buildConfig(functionResources, testConfig); - options.extensionEnv = params; - const functionEmuTriggerDefs: ParsedTriggerDefinition[] = functionResources.map((r) => - triggerHelper.functionResourceToEmulatedTriggerDefintion(r) - ); - options.extensionTriggers = functionEmuTriggerDefs; - options.extensionNodeVersion = specHelper.getNodeVersion(functionResources); - return options; + return secretEnvVar; } -// Exported for testing +/** + * Exported for testing + */ export function getParams(options: any, extensionSpec: ExtensionSpec) { const projectId = needProjectId(options); const userParams = paramHelper.readEnvFile(options.testParams); @@ -58,156 +104,8 @@ export function getParams(options: any, extensionSpec: ExtensionSpec) { const unsubbedParams = extensionsHelper.populateDefaultParams( unsubbedParamsWithoutDefaults, - extensionSpec.params + extensionSpec.params, ); // Run a substitution to support params that reference other params. return extensionsHelper.substituteParams>(unsubbedParams, unsubbedParams); } - -/** - * Checks and warns if the test config is missing fields - * that are relevant for the extension being emulated. - */ -function checkTestConfig(testConfig: { [key: string]: any }, functionResources: Resource[]) { - const logger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS); - if (!testConfig.functions && functionResources.length) { - logger.log( - "WARN", - "This extension uses functions," + - "but 'firebase.json' provided by --test-config is missing a top-level 'functions' object." + - "Functions will not be emulated." - ); - } - - if (!testConfig.firestore && shouldEmulateFirestore(functionResources)) { - logger.log( - "WARN", - "This extension interacts with Cloud Firestore," + - "but 'firebase.json' provided by --test-config is missing a top-level 'firestore' object." + - "Cloud Firestore will not be emulated." - ); - } - - if (!testConfig.database && shouldEmulateDatabase(functionResources)) { - logger.log( - "WARN", - "This extension interacts with Realtime Database," + - "but 'firebase.json' provided by --test-config is missing a top-level 'database' object." + - "Realtime Database will not be emulated." - ); - } - - if (!testConfig.storage && shouldEmulateStorage(functionResources)) { - logger.log( - "WARN", - "This extension interacts with Cloud Storage," + - "but 'firebase.json' provided by --test-config is missing a top-level 'storage' object." + - "Cloud Storage will not be emulated." - ); - } -} - -/** - * Reads a test config file. - * @param testConfigPath filepath to a firebase.json style config file. - */ -function readTestConfigFile(testConfigPath: string): { [key: string]: any } { - try { - const buf = fs.readFileSync(path.resolve(testConfigPath)); - return JSON.parse(buf.toString()); - } catch (err) { - throw new FirebaseError(`Error reading --test-config file: ${err.message}\n`, { - original: err, - }); - } -} - -function buildConfig( - functionResources: Resource[], - testConfig?: { [key: string]: string } -): Config { - const config = new Config(testConfig || {}, { projectDir: process.cwd(), cwd: process.cwd() }); - - const emulateFunctions = shouldEmulateFunctions(functionResources); - if (!testConfig) { - // If testConfig was provided, don't add any new blocks. - if (emulateFunctions) { - config.set("functions", {}); - } - if (shouldEmulateFirestore(functionResources)) { - config.set("firestore", {}); - } - if (shouldEmulateDatabase(functionResources)) { - config.set("database", {}); - } - if (shouldEmulatePubsub(functionResources)) { - config.set("pubsub", {}); - } - if (shouldEmulateStorage(functionResources)) { - config.set("storage", {}); - } - } - - if (config.src.functions) { - // Switch functions source to what is provided in the extension.yaml - // to match the behavior of deployed extensions. - const sourceDirectory = getFunctionSourceDirectory(functionResources); - config.set("functions.source", sourceDirectory); - } - return config; -} - -/** - * Finds the source directory from extension.yaml to use for emulating functions. - * Errors if the extension.yaml contins function resources with different or missing - * values for properties.sourceDirectory. - * @param functionResources An array of function type resources - */ -function getFunctionSourceDirectory(functionResources: Resource[]): string { - let sourceDirectory; - for (const r of functionResources) { - let dir = _.get(r, "properties.sourceDirectory"); - // If not specified, default sourceDirectory to "functions" - if (!dir) { - dir = "functions"; - } - if (!sourceDirectory) { - sourceDirectory = dir; - } else if (sourceDirectory != dir) { - throw new FirebaseError( - `Found function resources with different sourceDirectories: '${sourceDirectory}' and '${dir}'. The extensions emulator only supports a single sourceDirectory.` - ); - } - } - return sourceDirectory; -} - -function shouldEmulateFunctions(resources: Resource[]): boolean { - return resources.length > 0; -} - -function shouldEmulate(emulatorName: string, resources: Resource[]): boolean { - for (const r of resources) { - const eventType: string = _.get(r, "properties.eventTrigger.eventType", ""); - if (eventType.includes(emulatorName)) { - return true; - } - } - return false; -} - -function shouldEmulateFirestore(resources: Resource[]): boolean { - return shouldEmulate("cloud.firestore", resources); -} - -function shouldEmulateDatabase(resources: Resource[]): boolean { - return shouldEmulate("google.firebase.database", resources); -} - -function shouldEmulatePubsub(resources: Resource[]): boolean { - return shouldEmulate("google.pubsub", resources); -} - -function shouldEmulateStorage(resources: Resource[]): boolean { - return shouldEmulate("google.storage", resources); -} diff --git a/src/extensions/emulator/specHelper.spec.ts b/src/extensions/emulator/specHelper.spec.ts new file mode 100644 index 00000000000..7d6ac1cecdb --- /dev/null +++ b/src/extensions/emulator/specHelper.spec.ts @@ -0,0 +1,167 @@ +import { expect } from "chai"; + +import * as specHelper from "./specHelper"; +import { Resource } from "../types"; +import { FirebaseError } from "../../error"; +import { Runtime } from "../../deploy/functions/runtimes/supported"; +import { FIXTURE_DIR as MINIMAL_EXT_DIR } from "../../test/fixtures/extension-yamls/minimal"; +import { FIXTURE_DIR as HELLO_WORLD_EXT_DIR } from "../../test/fixtures/extension-yamls/hello-world"; + +const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + timeout: "3s", + location: "us-east1", + availableMemoryMb: 1024, + }, +}; + +describe("readExtensionYaml", () => { + const testCases: { + desc: string; + directory: string; + expected: any; // ExtensionSpec + }[] = [ + { + desc: "should read a minimal extension.yaml", + directory: MINIMAL_EXT_DIR, + expected: { + apis: [], + contributors: [], + description: "Sends the world a greeting.", + displayName: "Greet the world", + events: [], + externalServices: [], + license: "Apache-2.0", + lifecycleEvents: [], + name: "greet-the-world", + params: [], + resources: [], + roles: [], + specVersion: "v1beta", + systemParams: [], + version: "0.0.1", + }, + }, + { + desc: "should read a hello-world extension.yaml", + directory: HELLO_WORLD_EXT_DIR, + expected: { + apis: [], + billingRequired: true, + contributors: [], + description: "Sends the world a greeting.", + displayName: "Greet the world", + events: [], + externalServices: [], + license: "Apache-2.0", + lifecycleEvents: [], + name: "greet-the-world", + params: [ + { + default: "Hello", + description: + "What do you want to say to the world? For example, Hello world? or What's up, world?", + immutable: false, + label: "Greeting for the world", + param: "GREETING", + required: true, + type: "string", + }, + ], + resources: [ + { + description: + "HTTP request-triggered function that responds with a specified greeting message", + name: "greetTheWorld", + properties: { + httpsTrigger: {}, + runtime: "nodejs16", + }, + type: "firebaseextensions.v1beta.function", + }, + ], + roles: [], + sourceUrl: "https://github.com/ORG_OR_USER/REPO_NAME", + specVersion: "v1beta", + systemParams: [], + version: "0.0.1", + }, + }, + ]; + for (const tc of testCases) { + it(tc.desc, async () => { + const spec = await specHelper.readExtensionYaml(tc.directory); + expect(spec).to.deep.equal(tc.expected); + }); + } +}); + +describe("getRuntime", () => { + it("gets runtime of resources", () => { + const r1 = { + ...testResource, + properties: { + runtime: "nodejs14" as const, + }, + }; + const r2 = { + ...testResource, + properties: { + runtime: "nodejs14" as const, + }, + }; + expect(specHelper.getRuntime([r1, r2])).to.equal("nodejs14"); + }); + + it("chooses the latest runtime if many runtime exists", () => { + const r1 = { + ...testResource, + properties: { + runtime: "nodejs12" as const, + }, + }; + const r2 = { + ...testResource, + properties: { + runtime: "nodejs14" as const, + }, + }; + expect(specHelper.getRuntime([r1, r2])).to.equal("nodejs14"); + }); + + it("returns default runtime if none specified", () => { + const r1 = { + ...testResource, + properties: {}, + }; + const r2 = { + ...testResource, + properties: {}, + }; + expect(specHelper.getRuntime([r1, r2])).to.equal(specHelper.DEFAULT_RUNTIME); + }); + + it("returns default runtime given no resources", () => { + expect(specHelper.getRuntime([])).to.equal(specHelper.DEFAULT_RUNTIME); + }); + + it("throws error given invalid runtime", () => { + const r1 = { + ...testResource, + properties: { + // Note: as const won't work since this is actually an invalid runtime. + runtime: "dotnet6" as Runtime, + }, + }; + const r2 = { + ...testResource, + properties: { + runtime: "nodejs14" as const, + }, + }; + expect(() => specHelper.getRuntime([r1, r2])).to.throw(FirebaseError); + }); +}); diff --git a/src/extensions/emulator/specHelper.ts b/src/extensions/emulator/specHelper.ts index ab6f84a36cf..add2a3dad0f 100644 --- a/src/extensions/emulator/specHelper.ts +++ b/src/extensions/emulator/specHelper.ts @@ -1,35 +1,18 @@ -import * as yaml from "js-yaml"; -import * as _ from "lodash"; -import * as path from "path"; -import * as fs from "fs-extra"; - -import { ExtensionSpec, Resource } from "../extensionsApi"; +import * as supported from "../../deploy/functions/runtimes/supported"; +import { ExtensionSpec, Resource } from "../types"; import { FirebaseError } from "../../error"; import { substituteParams } from "../extensionsHelper"; -import { EmulatorLogger } from "../../emulator/emulatorLogger"; -import { Emulators } from "../../emulator/types"; +import { getResourceRuntime } from "../utils"; +import { readFileFromDirectory, wrappedSafeLoad } from "../../utils"; const SPEC_FILE = "extension.yaml"; +const POSTINSTALL_FILE = "POSTINSTALL.md"; const validFunctionTypes = [ "firebaseextensions.v1beta.function", + "firebaseextensions.v1beta.v2function", "firebaseextensions.v1beta.scheduledFunction", ]; -/** - * Wrapps `yaml.safeLoad` with an error handler to present better YAML parsing - * errors. - */ -function wrappedSafeLoad(source: string): any { - try { - return yaml.safeLoad(source); - } catch (err) { - if (err instanceof yaml.YAMLException) { - throw new FirebaseError(`YAML Error: ${err.message}`, { original: err }); - } - throw err; - } -} - /** * Reads an extension.yaml and parses its contents into an ExtensionSpec. * @param directory the directory to look for a extensionYaml in. @@ -37,103 +20,83 @@ function wrappedSafeLoad(source: string): any { export async function readExtensionYaml(directory: string): Promise { const extensionYaml = await readFileFromDirectory(directory, SPEC_FILE); const source = extensionYaml.source; - return wrappedSafeLoad(source); + const spec = wrappedSafeLoad(source); + // Ensure that any omitted array fields are initialized as empty arrays + spec.params = spec.params ?? []; + spec.systemParams = spec.systemParams ?? []; + spec.resources = spec.resources ?? []; + spec.apis = spec.apis ?? []; + spec.roles = spec.roles ?? []; + spec.externalServices = spec.externalServices ?? []; + spec.events = spec.events ?? []; + spec.lifecycleEvents = spec.lifecycleEvents ?? []; + spec.contributors = spec.contributors ?? []; + + return spec; } /** - * Retrieves a file from the directory. + * Reads a POSTINSTALL file and returns its content as a string + * @param directory the directory to look for POSTINSTALL.md in. */ -export function readFileFromDirectory( - directory: string, - file: string -): Promise<{ [key: string]: any }> { - return new Promise((resolve, reject) => { - fs.readFile(path.resolve(directory, file), "utf8", (err, data) => { - if (err) { - if (err.code === "ENOENT") { - return reject( - new FirebaseError(`Could not find "${file}" in "${directory}"`, { original: err }) - ); - } - reject( - new FirebaseError(`Failed to read file "${file}" in "${directory}"`, { original: err }) - ); - } else { - resolve(data); - } - }); - }).then((source) => { - return { - source, - sourceDirectory: directory, - }; - }); +export async function readPostinstall(directory: string): Promise { + const content = await readFileFromDirectory(directory, POSTINSTALL_FILE); + return content.source; } +/** + * Substitue parameters of function resources in the extensions spec. + */ export function getFunctionResourcesWithParamSubstitution( extensionSpec: ExtensionSpec, - params: { [key: string]: string } -): object[] { + params: { [key: string]: string }, +): Resource[] { const rawResources = extensionSpec.resources.filter((resource) => - validFunctionTypes.includes(resource.type) + validFunctionTypes.includes(resource.type), ); return substituteParams(rawResources, params); } +/** + * Get properties associated with the function resource. + */ export function getFunctionProperties(resources: Resource[]) { return resources.map((r) => r.properties); } +export const DEFAULT_RUNTIME: supported.Runtime = supported.latest("nodejs"); + /** - * Choses a node version to use based on the 'nodeVersion' field in resources. - * Currently, the emulator will use 1 node version for all functions, even though - * an extension can specify different node versions for each function when deployed. - * For now, we choose the newest version that a user lists in their function resources, - * and fall back to node 8 if none is listed. + * Get runtime associated with the resources. If multiple runtimes exists, choose the latest runtime. + * e.g. prefer nodejs14 over nodejs12. + * N.B. (inlined): I'm not sure why this code always assumes nodejs. It seems to + * work though and nobody is complaining that they can't run the Python + * emulator so I'm not investigating why it works. */ -export function getNodeVersion(resources: Resource[]): string { - const functionNamesWithoutRuntime: string[] = []; - const versions = resources.map((r: Resource) => { - if (_.includes(r.type, "function")) { - if (r.properties?.runtime) { - return r.properties?.runtime; - } else { - functionNamesWithoutRuntime.push(r.name); - } - } - return "nodejs8"; - }); - - if (functionNamesWithoutRuntime.length) { - EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled( - "WARN", - "extensions", - `No 'runtime' property found for the following functions, defaulting to nodejs8: ${functionNamesWithoutRuntime.join( - ", " - )}` - ); +export function getRuntime(resources: Resource[]): supported.Runtime { + if (resources.length === 0) { + return DEFAULT_RUNTIME; } - const invalidRuntimes = _.filter(versions, (v) => { - return !_.includes(v, "nodejs"); - }); + const invalidRuntimes: string[] = []; + const runtimes: supported.Runtime[] = resources.map((r: Resource) => { + const runtime = getResourceRuntime(r); + if (!runtime) { + return DEFAULT_RUNTIME; + } + if (!supported.runtimeIsLanguage(runtime, "nodejs")) { + invalidRuntimes.push(runtime); + return DEFAULT_RUNTIME; + } + return runtime; + }); if (invalidRuntimes.length) { throw new FirebaseError( `The following runtimes are not supported by the Emulator Suite: ${invalidRuntimes.join( - ", " - )}. \n Only Node runtimes are supported.` - ); - } - if (_.includes(versions, "nodejs10")) { - return "10"; - } - if (_.includes(versions, "nodejs6")) { - EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled( - "WARN", - "extensions", - "Node 6 is deprecated. We recommend upgrading to a newer version." + ", ", + )}. \n Only Node runtimes are supported.`, ); - return "6"; } - return "8"; + // Assumes that all runtimes target the nodejs. + return supported.latest("nodejs", runtimes); } diff --git a/src/extensions/emulator/triggerHelper.spec.ts b/src/extensions/emulator/triggerHelper.spec.ts new file mode 100644 index 00000000000..0db17b539e5 --- /dev/null +++ b/src/extensions/emulator/triggerHelper.spec.ts @@ -0,0 +1,291 @@ +import { expect } from "chai"; +import { ParsedTriggerDefinition } from "../../emulator/functionsEmulatorShared"; +import * as triggerHelper from "./triggerHelper"; +import { Resource } from "../types"; + +describe("triggerHelper", () => { + describe("functionResourceToEmulatedTriggerDefintion", () => { + it("should assign valid properties from the resource to the ETD and ignore others", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + timeout: "3s", + location: "us-east1", + availableMemoryMb: 1024, + }, + }; + (testResource.properties as Record).somethingInvalid = "a value"; + const expected = { + platform: "gcfv1", + availableMemoryMb: 1024, + entryPoint: "test-resource", + name: "test-resource", + regions: ["us-east1"], + timeoutSeconds: 3, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle HTTPS triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + httpsTrigger: {}, + }, + }; + const expected = { + platform: "gcfv1", + entryPoint: "test-resource", + name: "test-resource", + httpsTrigger: {}, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle firestore triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + eventTrigger: { + eventType: "providers/cloud.firestore/eventTypes/document.write", + resource: "myResource", + }, + }, + }; + const expected = { + platform: "gcfv1", + entryPoint: "test-resource", + name: "test-resource", + eventTrigger: { + service: "firestore.googleapis.com", + resource: "myResource", + eventType: "providers/cloud.firestore/eventTypes/document.write", + }, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle database triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + eventTrigger: { + eventType: "providers/google.firebase.database/eventTypes/ref.create", + resource: "myResource", + }, + }, + }; + const expected = { + platform: "gcfv1", + entryPoint: "test-resource", + name: "test-resource", + eventTrigger: { + eventType: "providers/google.firebase.database/eventTypes/ref.create", + service: "firebaseio.com", + resource: "myResource", + }, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle pubsub triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: "myResource", + }, + }, + }; + const expected = { + platform: "gcfv1", + entryPoint: "test-resource", + name: "test-resource", + eventTrigger: { + service: "pubsub.googleapis.com", + resource: "myResource", + eventType: "google.pubsub.topic.publish", + }, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle scheduled triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + scheduleTrigger: { + schedule: "every 5 minutes", + }, + }, + }; + const expected = { + platform: "gcfv1", + entryPoint: "test-resource", + name: "test-resource", + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: "", + }, + schedule: { + schedule: "every 5 minutes", + }, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle v2 custom event triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.v2function", + properties: { + eventTrigger: { + eventType: "test.custom.event", + channel: "projects/foo/locations/bar/channels/baz", + }, + }, + }; + const expected = { + platform: "gcfv2", + entryPoint: "test-resource", + name: "test-resource", + eventTrigger: { + service: "", + channel: "projects/foo/locations/bar/channels/baz", + eventType: "test.custom.event", + }, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should handle fully packed v2 triggers", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.v2function", + properties: { + buildConfig: { + runtime: "nodejs16", + }, + location: "us-cental1", + serviceConfig: { + availableMemory: "100MB", + minInstanceCount: 1, + maxInstanceCount: 10, + timeoutSeconds: 66, + }, + eventTrigger: { + eventType: "test.custom.event", + channel: "projects/foo/locations/bar/channels/baz", + pubsubTopic: "pubsub.topic", + eventFilters: [ + { + attribute: "basic", + value: "attr", + }, + { + attribute: "mattern", + value: "patch", + operator: "match-path-pattern", + }, + ], + retryPolicy: "RETRY", + triggerRegion: "us-cental1", + }, + }, + }; + const expected = { + platform: "gcfv2", + entryPoint: "test-resource", + name: "test-resource", + availableMemoryMb: 100, + timeoutSeconds: 66, + eventTrigger: { + service: "", + channel: "projects/foo/locations/bar/channels/baz", + eventType: "test.custom.event", + eventFilters: { + basic: "attr", + }, + eventFilterPathPatterns: { + mattern: "patch", + }, + }, + regions: ["us-cental1"], + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); + + expect(result).to.eql(expected); + }); + + it("should correctly inject system params", () => { + const testResource: Resource = { + name: "test-resource", + entryPoint: "functionName", + type: "firebaseextensions.v1beta.function", + properties: { + httpsTrigger: {}, + }, + }; + const systemParams = { + "firebaseextensions.v1beta.function/location": "us-west1", + "firebaseextensions.v1beta.function/memory": "1024", + "firebaseextensions.v1beta.function/timeoutSeconds": "70", + "firebaseextensions.v1beta.function/labels": "key:val,otherkey:otherval", + }; + const expected: ParsedTriggerDefinition = { + platform: "gcfv1", + entryPoint: "test-resource", + name: "test-resource", + availableMemoryMb: 1024, + timeoutSeconds: 70, + labels: { key: "val", otherkey: "otherval" }, + regions: ["us-west1"], + httpsTrigger: {}, + }; + + const result = triggerHelper.functionResourceToEmulatedTriggerDefintion( + testResource, + systemParams, + ); + + expect(result).to.eql(expected); + }); + }); +}); diff --git a/src/extensions/emulator/triggerHelper.ts b/src/extensions/emulator/triggerHelper.ts index 91f2526d1fb..5590ac74df6 100644 --- a/src/extensions/emulator/triggerHelper.ts +++ b/src/extensions/emulator/triggerHelper.ts @@ -1,37 +1,153 @@ -import * as _ from "lodash"; +import * as backend from "../../deploy/functions/backend"; +import { EmulatorLogger } from "../../emulator/emulatorLogger"; import { - ParsedTriggerDefinition, + EventSchedule, getServiceFromEventType, + ParsedTriggerDefinition, } from "../../emulator/functionsEmulatorShared"; -import { EmulatorLogger } from "../../emulator/emulatorLogger"; import { Emulators } from "../../emulator/types"; +import { FirebaseError } from "../../error"; +import { + FUNCTIONS_RESOURCE_TYPE, + FUNCTIONS_V2_RESOURCE_TYPE, + Resource, +} from "../../extensions/types"; +import * as proto from "../../gcp/proto"; -export function functionResourceToEmulatedTriggerDefintion(resource: any): ParsedTriggerDefinition { - const etd: ParsedTriggerDefinition = { - name: resource.name, - entryPoint: resource.name, - platform: "gcfv1", - }; - const properties = _.get(resource, "properties", {}); - if (properties.timeout) { - etd.timeout = properties.timeout; - } - if (properties.location) { - etd.regions = [properties.location]; - } - if (properties.availableMemoryMb) { - etd.availableMemoryMb = properties.availableMemoryMb; - } - if (properties.httpsTrigger) { - etd.httpsTrigger = properties.httpsTrigger; - } else if (properties.eventTrigger) { - properties.eventTrigger.service = getServiceFromEventType(properties.eventTrigger.eventType); - etd.eventTrigger = properties.eventTrigger; - } else { - EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( - "WARN", - `Function '${resource.name} is missing a trigger in extension.yaml. Please add one, as triggers defined in code are ignored.` +const SUPPORTED_SYSTEM_PARAMS = { + "firebaseextensions.v1beta.function": { + regions: "firebaseextensions.v1beta.function/location", + timeoutSeconds: "firebaseextensions.v1beta.function/timeoutSeconds", + availableMemoryMb: "firebaseextensions.v1beta.function/memory", + labels: "firebaseextensions.v1beta.function/labels", + }, +}; + +/** + * Convert a Resource into a ParsedTriggerDefinition + */ +export function functionResourceToEmulatedTriggerDefintion( + resource: Resource, + systemParams: Record = {}, +): ParsedTriggerDefinition { + const resourceType = resource.type; + if (resource.type === FUNCTIONS_RESOURCE_TYPE) { + const etd: ParsedTriggerDefinition = { + name: resource.name, + entryPoint: resource.name, + platform: "gcfv1", + }; + // These get used today in the emultor. + proto.convertIfPresent( + etd, + systemParams, + "regions", + SUPPORTED_SYSTEM_PARAMS[FUNCTIONS_RESOURCE_TYPE].regions, + (str: string) => [str], + ); + proto.convertIfPresent( + etd, + systemParams, + "timeoutSeconds", + SUPPORTED_SYSTEM_PARAMS[FUNCTIONS_RESOURCE_TYPE].timeoutSeconds, + (d) => +d, ); + proto.convertIfPresent( + etd, + systemParams, + "availableMemoryMb", + SUPPORTED_SYSTEM_PARAMS[FUNCTIONS_RESOURCE_TYPE].availableMemoryMb, + (d) => +d as backend.MemoryOptions, + ); + // These don't, but we inject them anyway for consistency and forward compatability + proto.convertIfPresent( + etd, + systemParams, + "labels", + SUPPORTED_SYSTEM_PARAMS[FUNCTIONS_RESOURCE_TYPE].labels, + (str: string): Record => { + const ret: Record = {}; + for (const [key, value] of str.split(",").map((label) => label.split(":"))) { + ret[key] = value; + } + return ret; + }, + ); + const properties = resource.properties || {}; + proto.convertIfPresent(etd, properties, "timeoutSeconds", "timeout", proto.secondsFromDuration); + proto.convertIfPresent(etd, properties, "regions", "location", (str: string) => [str]); + proto.copyIfPresent(etd, properties, "availableMemoryMb"); + if (properties.httpsTrigger !== undefined) { + // Need to explcitly check undefined since {} is falsey + etd.httpsTrigger = properties.httpsTrigger; + } + if (properties.eventTrigger) { + etd.eventTrigger = { + eventType: properties.eventTrigger.eventType, + resource: properties.eventTrigger.resource, + service: getServiceFromEventType(properties.eventTrigger.eventType), + }; + } else if (properties.scheduleTrigger) { + const schedule: EventSchedule = { + schedule: properties.scheduleTrigger.schedule, + }; + etd.schedule = schedule; + etd.eventTrigger = { + eventType: "google.pubsub.topic.publish", + resource: "", + }; + } else { + EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( + "WARN", + `Function '${resource.name} is missing a trigger in extension.yaml. Please add one, as triggers defined in code are ignored.`, + ); + } + return etd; + } + if (resource.type === FUNCTIONS_V2_RESOURCE_TYPE) { + const etd: ParsedTriggerDefinition = { + name: resource.name, + entryPoint: resource.name, + platform: "gcfv2", + }; + const properties = resource.properties || {}; + proto.convertIfPresent(etd, properties, "regions", "location", (str: string) => [str]); + if (properties.serviceConfig) { + proto.copyIfPresent(etd, properties.serviceConfig, "timeoutSeconds"); + proto.convertIfPresent( + etd, + properties.serviceConfig, + "availableMemoryMb", + "availableMemory", + (mem: string) => parseInt(mem) as backend.MemoryOptions, + ); + } + if (properties.eventTrigger) { + etd.eventTrigger = { + eventType: properties.eventTrigger.eventType, + service: getServiceFromEventType(properties.eventTrigger.eventType), + }; + proto.copyIfPresent(etd.eventTrigger, properties.eventTrigger, "channel"); + if (properties.eventTrigger.eventFilters) { + const eventFilters: Record = {}; + const eventFilterPathPatterns: Record = {}; + for (const filter of properties.eventTrigger.eventFilters) { + if (filter.operator === undefined) { + eventFilters[filter.attribute] = filter.value; + } else if (filter.operator === "match-path-pattern") { + eventFilterPathPatterns[filter.attribute] = filter.value; + } + } + etd.eventTrigger.eventFilters = eventFilters; + etd.eventTrigger.eventFilterPathPatterns = eventFilterPathPatterns; + } + } else { + EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( + "WARN", + `Function '${resource.name} is missing a trigger in extension.yaml. Please add one, as triggers defined in code are ignored.`, + ); + } + return etd; } - return etd; + throw new FirebaseError("Unexpected resource type " + resourceType); } diff --git a/src/extensions/etags.spec.ts b/src/extensions/etags.spec.ts new file mode 100644 index 00000000000..2dbe346014e --- /dev/null +++ b/src/extensions/etags.spec.ts @@ -0,0 +1,67 @@ +import { expect } from "chai"; + +import * as etags from "./etags"; +import * as rc from "../rc"; + +const TEST_PROJECT = "test-project"; + +function dummyRc(etagMap: Record) { + return new rc.RC(undefined, { + etags: { + "test-project": { + extensionInstances: etagMap, + }, + }, + }); +} + +function extensionInstanceHelper(instanceId: string, etag?: string) { + const ret = { + instanceId, + etag, + }; + return ret; +} + +describe("detectEtagChanges", () => { + const testCases: { + desc: string; + rc: rc.RC; + instances: { instanceId: string; etag?: string }[]; + expected: string[]; + }[] = [ + { + desc: "should not detect changes if there is no previously saved etags", + rc: dummyRc({}), + instances: [extensionInstanceHelper("test", "abc123")], + expected: [], + }, + { + desc: "should detect changes if a new instance was installed out of band", + rc: dummyRc({ test: "abc123" }), + instances: [ + extensionInstanceHelper("test", "abc123"), + extensionInstanceHelper("test2", "def456"), + ], + expected: ["test2"], + }, + { + desc: "should detect changes if an instance was changed out of band", + rc: dummyRc({ test: "abc123" }), + instances: [extensionInstanceHelper("test", "def546")], + expected: ["test"], + }, + { + desc: "should detect changes if an instance was deleted out of band", + rc: dummyRc({ test: "abc123" }), + instances: [], + expected: ["test"], + }, + ]; + for (const tc of testCases) { + it(tc.desc, () => { + const result = etags.detectEtagChanges(tc.rc, TEST_PROJECT, tc.instances); + expect(result).to.have.same.members(tc.expected); + }); + } +}); diff --git a/src/extensions/etags.ts b/src/extensions/etags.ts new file mode 100644 index 00000000000..1c96b23df5c --- /dev/null +++ b/src/extensions/etags.ts @@ -0,0 +1,45 @@ +import { RC } from "../rc"; + +export function saveEtags( + rc: RC, + projectId: string, + instances: { instanceId: string; etag?: string }[], +): void { + rc.setEtags(projectId, "extensionInstances", etagsMap(instances)); +} + +// detectEtagChanges compares the last set of etags stored in .firebaserc to the currently deployed etags, +// and returns the ids on any instances have different etags. +export function detectEtagChanges( + rc: RC, + projectId: string, + instances: { instanceId: string; etag?: string }[], +): string[] { + const lastDeployedEtags = rc.getEtags(projectId).extensionInstances; + const currentEtags = etagsMap(instances); + // If we don't have a record of the last deployed state, detect no changes. + if (!lastDeployedEtags || !Object.keys(lastDeployedEtags).length) { + return []; + } + // find any instances that changed since last deploy + const changedExtensionInstances = Object.entries(lastDeployedEtags) + .filter(([instanceName, etag]) => etag !== currentEtags[instanceName]) + .map((i) => i[0]); + // find any instances that we installed out of band since last deploy + const newExtensionInstances = Object.keys(currentEtags).filter( + (instanceName) => !lastDeployedEtags[instanceName], + ); + return newExtensionInstances.concat(changedExtensionInstances); +} + +function etagsMap(instances: { instanceId: string; etag?: string }[]): Record { + return instances.reduce( + (acc, i) => { + if (i.etag) { + acc[i.instanceId] = i.etag; + } + return acc; + }, + {} as Record, + ); +} diff --git a/src/test/extensions/export.spec.ts b/src/extensions/export.spec.ts similarity index 89% rename from src/test/extensions/export.spec.ts rename to src/extensions/export.spec.ts index 5a151ec4049..51050820ff3 100644 --- a/src/test/extensions/export.spec.ts +++ b/src/extensions/export.spec.ts @@ -1,10 +1,8 @@ import { expect } from "chai"; -import * as sinon from "sinon"; -import { parameterizeProject, setSecretParamsToLatest } from "../../extensions/export"; -import { InstanceSpec } from "../../deploy/extensions/planner"; -import * as secretUtils from "../../extensions/secretsUtils"; -import { ParamType } from "../../extensions/extensionsApi"; +import { parameterizeProject, setSecretParamsToLatest } from "./export"; +import { DeploymentInstanceSpec } from "../deploy/extensions/planner"; +import { ParamType } from "./types"; describe("ext:export helpers", () => { describe("parameterizeProject", () => { @@ -54,11 +52,13 @@ describe("ext:export helpers", () => { const testSpec = { instanceId: "my-instance", params: t.in, + systemParams: {}, }; expect(parameterizeProject(TEST_PROJECT_ID, TEST_PROJECT_NUMBER, testSpec)).to.deep.equal({ instanceId: testSpec.instanceId, params: t.expected, + systemParams: {}, }); }); } @@ -79,9 +79,10 @@ describe("ext:export helpers", () => { ]; for (const t of tests) { it(t.desc, async () => { - const testSpec: InstanceSpec = { + const testSpec: DeploymentInstanceSpec = { instanceId: "my-instance", params: t.params, + systemParams: {}, extensionVersion: { name: "test", ref: "test/test@0.1.0", @@ -104,6 +105,7 @@ describe("ext:export helpers", () => { label: "blah", }, ], + systemParams: [], }, }, }; diff --git a/src/extensions/export.ts b/src/extensions/export.ts index 6da6b63851e..2f4317c2049 100644 --- a/src/extensions/export.ts +++ b/src/extensions/export.ts @@ -1,17 +1,8 @@ -import * as clc from "cli-color"; - -import * as refs from "./refs"; -import { getProjectNumber } from "../getProjectNumber"; -import { Options } from "../options"; -import { Config } from "../config"; -import { getExtensionVersion, InstanceSpec } from "../deploy/extensions/planner"; +import { getExtensionVersion, DeploymentInstanceSpec } from "../deploy/extensions/planner"; import { humanReadable } from "../deploy/extensions/deploymentSummary"; import { logger } from "../logger"; -import { FirebaseError } from "../error"; -import { promptOnce } from "../prompt"; import { parseSecretVersionResourceName, toSecretVersionResourceName } from "../gcp/secretManager"; import { getActiveSecrets } from "./secretsUtils"; - /** * parameterizeProject searchs spec.params for any param that include projectId or projectNumber, * and replaces it with a parameterized version that can be used on other projects. @@ -20,8 +11,8 @@ import { getActiveSecrets } from "./secretsUtils"; export function parameterizeProject( projectId: string, projectNumber: string, - spec: InstanceSpec -): InstanceSpec { + spec: DeploymentInstanceSpec, +): DeploymentInstanceSpec { const newParams: Record = {}; for (const [key, val] of Object.entries(spec.params)) { const p1 = val.replace(projectId, "${param:PROJECT_ID}"); @@ -37,7 +28,9 @@ export function parameterizeProject( * setSecretParamsToLatest searches spec.params for any secret paramsthat are active, and changes their version to latest. * We do this because old secret versions are destroyed on instance update, and to ensure that cross project installs work smoothly. */ -export async function setSecretParamsToLatest(spec: InstanceSpec): Promise { +export async function setSecretParamsToLatest( + spec: DeploymentInstanceSpec, +): Promise { const newParams = { ...spec.params }; const extensionVersion = await getExtensionVersion(spec); const activeSecrets = getActiveSecrets(extensionVersion.spec, newParams); @@ -51,7 +44,10 @@ export async function setSecretParamsToLatest(spec: InstanceSpec): Promise `${r[0]}=${r[1]}`) - .join("\n"); - await existingConfig.askWriteProjectFile(`extensions/${spec.instanceId}.env`, content, force); -} - -export async function writeFiles(have: InstanceSpec[], options: Options) { - const existingConfig = Config.load(options, true); - if (!existingConfig) { - throw new FirebaseError( - "Not currently in a Firebase directory. Please run `firebase init` to create a Firebase directory." - ); - } - if ( - existingConfig.has("extensions") && - Object.keys(existingConfig.get("extensions")).length && - !options.nonInteractive && - !options.force - ) { - const currentExtensions = Object.entries(existingConfig.get("extensions")) - .map((i) => `${i[0]}: ${i[1]}`) - .join("\n\t"); - const overwrite = await promptOnce({ - type: "list", - message: `firebase.json already contains extensions:\n${currentExtensions}\nWould you like to overwrite or merge?`, - choices: [ - { name: "Overwrite", value: true }, - { name: "Merge", value: false }, - ], - }); - if (overwrite) { - existingConfig.set("extensions", {}); + if (spec.allowedEventTypes?.length) { + logger.info(`\tALLOWED_EVENTS=${spec.allowedEventTypes}`); } - } - writeExtensionsToFirebaseJson(have, existingConfig); - for (const spec of have) { - await writeEnvFile(spec, existingConfig, options.force); + if (spec.eventarcChannel) { + logger.info(`\tEVENTARC_CHANNEL=${spec.eventarcChannel}`); + } + logger.info(""); } } diff --git a/src/extensions/extensionsApi.spec.ts b/src/extensions/extensionsApi.spec.ts new file mode 100644 index 00000000000..0f11b4dd9ca --- /dev/null +++ b/src/extensions/extensionsApi.spec.ts @@ -0,0 +1,909 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import * as api from "../api"; +import { FirebaseError } from "../error"; +import * as extensionsApi from "./extensionsApi"; +import { ExtensionSource } from "./types"; +import { cloneDeep } from "../utils"; + +const VERSION = "v1beta"; +const PROJECT_ID = "test-project"; +const INSTANCE_ID = "test-extensions-instance"; +const PUBLISHER_ID = "test-project"; +const EXTENSION_ID = "test-extension"; +const EXTENSION_VERSION = "0.0.1"; + +const EXT_SPEC = { + name: "cool-things", + version: "1.0.0", + resources: { + name: "cool-resource", + type: "firebaseextensions.v1beta.function", + }, + sourceUrl: "www.google.com/cool-things-here", +}; +const TEST_EXTENSION_1 = { + name: "publishers/test-pub/extensions/ext-one", + ref: "test-pub/ext-one", + state: "PUBLISHED", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXTENSION_2 = { + name: "publishers/test-pub/extensions/ext-two", + ref: "test-pub/ext-two", + state: "PUBLISHED", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXTENSION_3 = { + name: "publishers/test-pub/extensions/ext-three", + ref: "test-pub/ext-three", + state: "UNPUBLISHED", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXT_VERSION_1 = { + name: "publishers/test-pub/extensions/ext-one/versions/0.0.1", + ref: "test-pub/ext-one@0.0.1", + spec: EXT_SPEC, + state: "UNPUBLISHED", + hash: "12345", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXT_VERSION_2 = { + name: "publishers/test-pub/extensions/ext-one/versions/0.0.2", + ref: "test-pub/ext-one@0.0.2", + spec: EXT_SPEC, + state: "PUBLISHED", + hash: "23456", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXT_VERSION_3 = { + name: "publishers/test-pub/extensions/ext-one/versions/0.0.3", + ref: "test-pub/ext-one@0.0.3", + spec: EXT_SPEC, + state: "PUBLISHED", + hash: "34567", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_INSTANCE_1 = { + name: "projects/invader-zim/instances/image-resizer-1", + createTime: "2019-06-19T00:20:10.416947Z", + updateTime: "2019-06-19T00:21:06.722782Z", + state: "ACTIVE", + config: { + name: "projects/invader-zim/instances/image-resizer-1/configurations/5b1fb749-764d-4bd1-af60-bb7f22d27860", + createTime: "2019-06-19T00:21:06.722782Z", + }, +}; + +const TEST_INSTANCE_2 = { + name: "projects/invader-zim/instances/image-resizer", + createTime: "2019-05-19T00:20:10.416947Z", + updateTime: "2019-05-19T00:20:10.416947Z", + state: "ACTIVE", + config: { + name: "projects/invader-zim/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + }, +}; + +const TEST_INSTANCES_RESPONSE = { + instances: [TEST_INSTANCE_1, TEST_INSTANCE_2], +}; + +const TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN: any = cloneDeep(TEST_INSTANCES_RESPONSE); +TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN.nextPageToken = "abc123"; + +const PACKAGE_URI = "https://storage.googleapis.com/ABCD.zip"; +const SOURCE_NAME = "projects/firebasemods/sources/abcd"; +const TEST_SOURCE = { + name: SOURCE_NAME, + packageUri: PACKAGE_URI, + hash: "deadbeef", + spec: { + name: "test", + displayName: "Old", + description: "descriptive", + version: "1.0.0", + license: "MIT", + resources: [ + { + name: "resource1", + type: "firebaseextensions.v1beta.function", + description: "desc", + propertiesYaml: + "eventTrigger:\n eventType: providers/cloud.firestore/eventTypes/document.write\n resource: projects/${PROJECT_ID}/databases/(default)/documents/${COLLECTION_PATH}/{documentId}\nlocation: ${LOCATION}", + }, + ], + author: { authorName: "Tester" }, + contributors: [{ authorName: "Tester 2" }], + billingRequired: true, + sourceUrl: "test.com", + params: [], + }, +}; + +const NEXT_PAGE_TOKEN = "random123"; +const PUBLISHED_EXTENSIONS = { extensions: [TEST_EXTENSION_1, TEST_EXTENSION_2] }; +const ALL_EXTENSIONS = { + extensions: [TEST_EXTENSION_1, TEST_EXTENSION_2, TEST_EXTENSION_3], +}; +const PUBLISHED_WITH_TOKEN = { extensions: [TEST_EXTENSION_1], nextPageToken: NEXT_PAGE_TOKEN }; +const NEXT_PAGE_EXTENSIONS = { extensions: [TEST_EXTENSION_2] }; + +const PUBLISHED_EXT_VERSIONS = { extensionVersions: [TEST_EXT_VERSION_2, TEST_EXT_VERSION_3] }; +const ALL_EXT_VERSIONS = { + extensionVersions: [TEST_EXT_VERSION_1, TEST_EXT_VERSION_2, TEST_EXT_VERSION_3], +}; +const PUBLISHED_VERSIONS_WITH_TOKEN = { + extensionVersions: [TEST_EXT_VERSION_2], + nextPageToken: NEXT_PAGE_TOKEN, +}; +const NEXT_PAGE_VERSIONS = { extensionVersions: [TEST_EXT_VERSION_3] }; + +describe("extensions", () => { + describe("listInstances", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should return a list of installed extensions instances", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, TEST_INSTANCES_RESPONSE); + + const instances = await extensionsApi.listInstances(PROJECT_ID); + + expect(instances).to.deep.equal(TEST_INSTANCES_RESPONSE.instances); + expect(nock.isDone()).to.be.true; + }); + + it("should query for more installed extensions if the response has a next_page_token", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN); + nock(api.extensionsOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) + .query((queryParams: any) => { + return queryParams.pageToken === "abc123"; + }) + .reply(200, TEST_INSTANCES_RESPONSE); + + const instances = await extensionsApi.listInstances(PROJECT_ID); + + const expected = TEST_INSTANCES_RESPONSE.instances.concat( + TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN.instances, + ); + expect(instances).to.deep.equal(expected); + expect(nock.isDone()).to.be.true; + }); + + it("should throw FirebaseError if any call returns an error", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN); + nock(api.extensionsOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) + .query((queryParams: any) => { + return queryParams.pageToken === "abc123"; + }) + .reply(503); + + await expect(extensionsApi.listInstances(PROJECT_ID)).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createInstance", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a POST call to the correct endpoint, and then poll on the returned operation when given a source", async () => { + nock(api.extensionsOrigin()) + .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) + .query({ validateOnly: "false" }) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); + + await extensionsApi.createInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: { + state: "ACTIVE", + name: "sources/blah", + packageUri: "https://test.fake/pacakge.zip", + hash: "abc123", + spec: { + name: "", + version: "0.1.0", + sourceUrl: "", + roles: [], + resources: [], + params: [], + systemParams: [], + }, + }, + params: {}, + systemParams: {}, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a POST call to the correct endpoint, and then poll on the returned operation when given an Extension ref", async () => { + nock(api.extensionsOrigin()) + .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) + .query({ validateOnly: "false" }) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); + + await extensionsApi.createInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionVersionRef: "test-pub/test-ext@0.1.0", + params: {}, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a POST and not poll if validateOnly=true", async () => { + nock(api.extensionsOrigin()) + .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) + .query({ validateOnly: "true" }) + .reply(200, { name: "operations/abc123", done: true }); + + await extensionsApi.createInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionVersionRef: "test-pub/test-ext@0.1.0", + params: {}, + validateOnly: true, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if create returns an error response", async () => { + nock(api.extensionsOrigin()) + .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) + .query({ validateOnly: "false" }) + .reply(500); + + await expect( + extensionsApi.createInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: { + state: "ACTIVE", + name: "sources/blah", + packageUri: "https://test.fake/pacakge.zip", + hash: "abc123", + spec: { + name: "", + version: "0.1.0", + sourceUrl: "", + roles: [], + resources: [], + params: [], + systemParams: [], + }, + }, + params: {}, + }), + ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500, Unknown Error"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("configureInstance", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a PATCH call to the correct endpoint, and then poll on the returned operation", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: "config.params,config.allowed_event_types,config.eventarc_channel", + validateOnly: "false", + }) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()) + .get(`/${VERSION}/operations/abc123`) + .reply(200, { done: false }) + .get(`/${VERSION}/operations/abc123`) + .reply(200, { done: true }); + + await extensionsApi.configureInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + params: { MY_PARAM: "value" }, + canEmitEvents: false, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a PATCH and not poll if validateOnly=true", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: "config.params,config.allowed_event_types,config.eventarc_channel", + validateOnly: "true", + }) + .reply(200, { name: "operations/abc123", done: true }); + + await extensionsApi.configureInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + params: { MY_PARAM: "value" }, + validateOnly: true, + canEmitEvents: false, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if update returns an error response", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: "config.params,config.allowed_event_types,config.eventarc_channel", + validateOnly: false, + }) + .reply(500); + + await expect( + extensionsApi.configureInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + params: { MY_PARAM: "value" }, + canEmitEvents: false, + }), + ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deleteInstance", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a DELETE call to the correct endpoint, and then poll on the returned operation", async () => { + nock(api.extensionsOrigin()) + .delete(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); + + await extensionsApi.deleteInstance(PROJECT_ID, INSTANCE_ID); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if delete returns an error response", async () => { + nock(api.extensionsOrigin()) + .delete(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(404); + + await expect(extensionsApi.deleteInstance(PROJECT_ID, INSTANCE_ID)).to.be.rejectedWith( + FirebaseError, + ); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateInstance", () => { + const testSource: ExtensionSource = { + state: "ACTIVE", + name: "abc123", + packageUri: "www.google.com/pack.zip", + hash: "abc123", + spec: { + name: "abc123", + version: "0.1.0", + resources: [], + params: [], + systemParams: [], + sourceUrl: "www.google.com/pack.zip", + }, + }; + afterEach(() => { + nock.cleanAll(); + }); + + it("should include config.params in updateMask is params are changed", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: + "config.source.name,config.params,config.allowed_event_types,config.eventarc_channel", + validateOnly: "false", + }) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); + + await extensionsApi.updateInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: testSource, + params: { + MY_PARAM: "value", + }, + canEmitEvents: false, + }); + + expect(nock.isDone()).to.be.true; + }); + + it("should not include config.params or config.system_params in updateMask is params aren't changed", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: "config.source.name,config.allowed_event_types,config.eventarc_channel", + validateOnly: "false", + }) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); + + await extensionsApi.updateInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: testSource, + canEmitEvents: false, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should include config.system_params in updateMask if system_params are changed", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: + "config.source.name,config.system_params,config.allowed_event_types,config.eventarc_channel", + validateOnly: "false", + }) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); + + await extensionsApi.updateInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: testSource, + systemParams: { + MY_PARAM: "value", + }, + canEmitEvents: false, + }); + + expect(nock.isDone()).to.be.true; + }); + + it("should include config.allowed_event_types and config.eventarc_Channel in updateMask if events config is provided", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: + "config.source.name,config.params,config.allowed_event_types,config.eventarc_channel", + validateOnly: "false", + }) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); + + await extensionsApi.updateInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: testSource, + params: { + MY_PARAM: "value", + }, + canEmitEvents: true, + eventarcChannel: "projects/${PROJECT_ID}/locations/us-central1/channels/firebase", + allowedEventTypes: ["google.firebase.custom-events-occurred"], + }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a PATCH and not poll if validateOnly=true", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: "config.source.name,config.allowed_event_types,config.eventarc_channel", + validateOnly: "true", + }) + .reply(200, { name: "operations/abc123", done: true }); + + await extensionsApi.updateInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: testSource, + validateOnly: true, + canEmitEvents: false, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should make a PATCH and not poll if validateOnly=true", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: "config.source.name,config.allowed_event_types,config.eventarc_channel", + validateOnly: "true", + }) + .reply(200, { name: "operations/abc123", done: true }); + + await extensionsApi.updateInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: testSource, + validateOnly: true, + canEmitEvents: false, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if update returns an error response", async () => { + nock(api.extensionsOrigin()) + .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .query({ + updateMask: + "config.source.name,config.params,config.allowed_event_types,config.eventarc_channel", + validateOnly: false, + }) + .reply(500); + + await expect( + extensionsApi.updateInstance({ + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + extensionSource: testSource, + params: { + MY_PARAM: "value", + }, + canEmitEvents: false, + }), + ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500"); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getInstance", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a GET call to the correct endpoint", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(200); + + await extensionsApi.getInstance(PROJECT_ID, INSTANCE_ID); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(404); + + await expect(extensionsApi.getInstance(PROJECT_ID, INSTANCE_ID)).to.be.rejectedWith( + FirebaseError, + ); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getSource", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a GET call to the correct endpoint", async () => { + nock(api.extensionsOrigin()).get(`/${VERSION}/${SOURCE_NAME}`).reply(200, TEST_SOURCE); + + const source = await extensionsApi.getSource(SOURCE_NAME); + expect(nock.isDone()).to.be.true; + expect(source.spec.resources).to.have.lengthOf(1); + expect(source.spec.resources[0]).to.have.property("properties"); + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsOrigin()).get(`/${VERSION}/${SOURCE_NAME}`).reply(404); + + await expect(extensionsApi.getSource(SOURCE_NAME)).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createSource", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a POST call to the correct endpoint, and then poll on the returned operation", async () => { + nock(api.extensionsOrigin()) + .post(`/${VERSION}/projects/${PROJECT_ID}/sources/`) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()) + .get(`/${VERSION}/operations/abc123`) + .reply(200, { done: true, response: TEST_SOURCE }); + + const source = await extensionsApi.createSource(PROJECT_ID, PACKAGE_URI, ",./"); + expect(nock.isDone()).to.be.true; + expect(source.spec.resources).to.have.lengthOf(1); + expect(source.spec.resources[0]).to.have.property("properties"); + }); + + it("should throw a FirebaseError if create returns an error response", async () => { + nock(api.extensionsOrigin()).post(`/${VERSION}/projects/${PROJECT_ID}/sources/`).reply(500); + + await expect(extensionsApi.createSource(PROJECT_ID, PACKAGE_URI, "./")).to.be.rejectedWith( + FirebaseError, + "HTTP Error: 500, Unknown Error", + ); + expect(nock.isDone()).to.be.true; + }); + + it("stop polling and throw if the operation call throws an unexpected error", async () => { + nock(api.extensionsOrigin()) + .post(`/${VERSION}/projects/${PROJECT_ID}/sources/`) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsOrigin()).get(`/${VERSION}/operations/abc123`).reply(502, {}); + + await expect(extensionsApi.createSource(PROJECT_ID, PACKAGE_URI, "./")).to.be.rejectedWith( + FirebaseError, + "HTTP Error: 502, Unknown Error", + ); + expect(nock.isDone()).to.be.true; + }); + }); +}); + +describe("getExtension", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a GET call to the correct endpoint", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) + .reply(200); + + await extensionsApi.getExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) + .reply(404); + + await expect(extensionsApi.getExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`)).to.be.rejectedWith( + FirebaseError, + ); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect(extensionsApi.getExtension(`${PUBLISHER_ID}`)).to.be.rejectedWith( + FirebaseError, + "Unable to parse", + ); + }); +}); + +describe("getExtensionVersion", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a GET call to the correct endpoint", async () => { + nock(api.extensionsOrigin()) + .get( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions/${EXTENSION_VERSION}`, + ) + .reply(200, TEST_EXTENSION_1); + + const got = await extensionsApi.getExtensionVersion( + `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, + ); + expect(got).to.deep.equal(TEST_EXTENSION_1); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsOrigin()) + .get( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions/${EXTENSION_VERSION}`, + ) + .reply(404); + + await expect( + extensionsApi.getExtensionVersion(`${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`), + ).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect( + extensionsApi.getExtensionVersion(`${PUBLISHER_ID}//${EXTENSION_ID}`), + ).to.be.rejectedWith(FirebaseError, "Unable to parse"); + }); +}); + +describe("listExtensions", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should return a list of published extensions", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(200, PUBLISHED_EXTENSIONS); + + const extensions = await extensionsApi.listExtensions(PUBLISHER_ID); + expect(extensions).to.deep.equal(PUBLISHED_EXTENSIONS.extensions); + expect(nock.isDone()).to.be.true; + }); + + it("should return a list of all extensions", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(200, ALL_EXTENSIONS); + + const extensions = await extensionsApi.listExtensions(PUBLISHER_ID); + + expect(extensions).to.deep.equal(ALL_EXTENSIONS.extensions); + expect(nock.isDone()).to.be.true; + }); + + it("should query for more extensions if the response has a next_page_token", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(200, PUBLISHED_WITH_TOKEN); + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + queryParams.pageToken === NEXT_PAGE_TOKEN; + return queryParams; + }) + .reply(200, NEXT_PAGE_EXTENSIONS); + + const extensions = await extensionsApi.listExtensions(PUBLISHER_ID); + + const expected = PUBLISHED_WITH_TOKEN.extensions.concat(NEXT_PAGE_EXTENSIONS.extensions); + expect(extensions).to.deep.equal(expected); + expect(nock.isDone()).to.be.true; + }); + + it("should throw FirebaseError if any call returns an error", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(503, PUBLISHED_EXTENSIONS); + + await expect(extensionsApi.listExtensions(PUBLISHER_ID)).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); +}); + +describe("listExtensionVersions", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should return a list of published extension versions", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, PUBLISHED_EXT_VERSIONS); + + const extensions = await extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); + expect(extensions).to.deep.equal(PUBLISHED_EXT_VERSIONS.extensionVersions); + expect(nock.isDone()).to.be.true; + }); + + it("should send filter query param", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100" && queryParams.filter === "id<1.0.0"; + }) + .reply(200, PUBLISHED_EXT_VERSIONS); + + const extensions = await extensionsApi.listExtensionVersions( + `${PUBLISHER_ID}/${EXTENSION_ID}`, + "id<1.0.0", + ); + expect(extensions).to.deep.equal(PUBLISHED_EXT_VERSIONS.extensionVersions); + expect(nock.isDone()).to.be.true; + }); + + it("should return a list of all extension versions", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, ALL_EXT_VERSIONS); + + const extensions = await extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); + + expect(extensions).to.deep.equal(ALL_EXT_VERSIONS.extensionVersions); + expect(nock.isDone()).to.be.true; + }); + + it("should query for more extension versions if the response has a next_page_token", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100" && queryParams.pageToken === NEXT_PAGE_TOKEN; + }) + .reply(200, NEXT_PAGE_VERSIONS); + + const extensions = await extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); + + const expected = PUBLISHED_VERSIONS_WITH_TOKEN.extensionVersions.concat( + NEXT_PAGE_VERSIONS.extensionVersions, + ); + expect(extensions).to.deep.equal(expected); + expect(nock.isDone()).to.be.true; + }); + + it("should throw FirebaseError if any call returns an error", async () => { + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); + nock(api.extensionsOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100" && queryParams.pageToken === NEXT_PAGE_TOKEN; + }) + .reply(500); + + await expect( + extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`), + ).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect(extensionsApi.listExtensionVersions("")).to.be.rejectedWith( + FirebaseError, + "Unable to parse", + ); + }); +}); diff --git a/src/extensions/extensionsApi.ts b/src/extensions/extensionsApi.ts index 5ec59b7a24a..5d1a9a65cc1 100644 --- a/src/extensions/extensionsApi.ts +++ b/src/extensions/extensionsApi.ts @@ -1,169 +1,27 @@ -import * as yaml from "js-yaml"; -import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as marked from "marked"; -import * as api from "../api"; -import * as refs from "./refs"; +import * as yaml from "yaml"; +import * as clc from "colorette"; + +import { Client } from "../apiv2"; +import { extensionsOrigin } from "../api"; +import { FirebaseError } from "../error"; import { logger } from "../logger"; import * as operationPoller from "../operation-poller"; -import { FirebaseError } from "../error"; - -const VERSION = "v1beta"; +import * as refs from "./refs"; +import { + Extension, + ExtensionInstance, + ExtensionSource, + ExtensionSpec, + ExtensionVersion, +} from "./types"; + +const EXTENSIONS_API_VERSION = "v1beta"; const PAGE_SIZE_MAX = 100; -export enum RegistryLaunchStage { - EXPERIMENTAL = "EXPERIMENTAL", - BETA = "BETA", - GA = "GA", - DEPRECATED = "DEPRECATED", - REGISTRY_LAUNCH_STAGE_UNSPECIFIED = "REGISTRY_LAUNCH_STAGE_UNSPECIFIED", -} - -export enum Visibility { - UNLISTED = "unlisted", - PUBLIC = "public", -} - -export interface Extension { - name: string; - ref: string; - visibility: Visibility; - registryLaunchStage: RegistryLaunchStage; - createTime: string; - latestVersion?: string; - latestVersionCreateTime?: string; -} - -export interface ExtensionVersion { - name: string; - ref: string; - state: "STATE_UNSPECIFIED" | "PUBLISHED" | "DEPRECATED"; - spec: ExtensionSpec; - hash: string; - sourceDownloadUri: string; - releaseNotes?: string; - createTime?: string; - deprecationMessage?: string; -} - -export interface PublisherProfile { - name: string; - publisherId: string; - registerTime: string; -} - -export interface ExtensionInstance { - name: string; - createTime: string; - updateTime: string; - state: "STATE_UNSPECIFIED" | "DEPLOYING" | "UNINSTALLING" | "ACTIVE" | "ERRORED" | "PAUSED"; - config: ExtensionConfig; - serviceAccountEmail: string; - errorStatus?: string; - lastOperationName?: string; - lastOperationType?: string; - extensionRef?: string; - extensionVersion?: string; -} - -export interface ExtensionConfig { - name: string; - createTime: string; - source: ExtensionSource; - params: { - [key: string]: any; - }; - populatedPostinstallContent?: string; - extensionRef?: string; - extensionVersion?: string; -} - -export interface ExtensionSource { - state: "STATE_UNSPECIFIED" | "ACTIVE" | "DELETED"; - name: string; - packageUri: string; - hash: string; - spec: ExtensionSpec; - extensionRoot?: string; - fetchTime?: string; - lastOperationName?: string; -} - -export interface ExtensionSpec { - specVersion?: string; - name: string; - version: string; - displayName?: string; - description?: string; - apis?: Api[]; - roles?: Role[]; - resources: Resource[]; - billingRequired?: boolean; - author?: Author; - contributors?: Author[]; - license?: string; - releaseNotesUrl?: string; - sourceUrl: string; - params: Param[]; - preinstallContent?: string; - postinstallContent?: string; - readmeContent?: string; - externalServices?: ExternalService[]; -} - -export interface ExternalService { - name: string; - pricingUri: string; -} - -export interface Api { - apiName: string; - reason: string; -} - -export interface Role { - role: string; - reason: string; -} - -export interface Resource { - name: string; - type: string; - description?: string; - properties?: { [key: string]: any }; - propertiesYaml?: string; -} - -export interface Author { - authorName: string; - url?: string; -} - -export interface Param { - param: string; - label: string; - description?: string; - default?: string; - type?: ParamType; - options?: ParamOption[]; - required?: boolean; - validationRegex?: string; - validationErrorMessage?: string; - immutable?: boolean; - example?: string; -} - -export enum ParamType { - STRING = "STRING", - SELECT = "SELECT", - MULTISELECT = "MULTISELECT", - SECRET = "SECRET", -} - -export interface ParamOption { - value: string; - label?: string; -} +const extensionsApiClient = new Client({ + urlPrefix: extensionsOrigin(), + apiVersion: EXTENSIONS_API_VERSION, +}); /** * Create a new extension instance, given a extension source path or extension reference, a set of params, and a service account. @@ -176,27 +34,31 @@ async function createInstanceHelper( projectId: string, instanceId: string, config: any, - validateOnly: boolean = false + validateOnly = false, ): Promise { - const createRes = await api.request("POST", `/${VERSION}/projects/${projectId}/instances/`, { - auth: true, - origin: api.extensionsOrigin, - data: { + const createRes = await extensionsApiClient.post< + { name: string; config: unknown }, + ExtensionInstance + >( + `/projects/${projectId}/instances/`, + { name: `projects/${projectId}/instances/${instanceId}`, - config: config, + config, }, - query: { - validateOnly, + { + queryParams: { + validateOnly: validateOnly ? "true" : "false", + }, }, - }); + ); if (validateOnly) { - return createRes; + return createRes.body; } const pollRes = await operationPoller.pollOperation({ - apiOrigin: api.extensionsOrigin, - apiVersion: VERSION, + apiOrigin: extensionsOrigin(), + apiVersion: EXTENSIONS_API_VERSION, operationResourceName: createRes.body.name, - masterTimeout: 600000, + masterTimeout: 3600000, }); return pollRes; } @@ -215,16 +77,22 @@ export async function createInstance(args: { instanceId: string; extensionSource?: ExtensionSource; extensionVersionRef?: string; - params: { [key: string]: string }; + params: Record; + systemParams?: Record; + allowedEventTypes?: string[]; + eventarcChannel?: string; validateOnly?: boolean; }): Promise { const config: any = { params: args.params, + systemParams: args.systemParams ?? {}, + allowedEventTypes: args.allowedEventTypes, + eventarcChannel: args.eventarcChannel, }; if (args.extensionSource && args.extensionVersionRef) { throw new FirebaseError( - "ExtensionSource and ExtensionVersion both provided, but only one should be." + "ExtensionSource and ExtensionVersion both provided, but only one should be.", ); } else if (args.extensionSource) { config.source = { name: args.extensionSource?.name }; @@ -235,6 +103,12 @@ export async function createInstance(args: { } else { throw new FirebaseError("No ExtensionVersion or ExtensionSource provided but one is required."); } + if (args.allowedEventTypes) { + config.allowedEventTypes = args.allowedEventTypes; + } + if (args.eventarcChannel) { + config.eventarcChannel = args.eventarcChannel; + } return createInstanceHelper(args.projectId, args.instanceId, config, args.validateOnly); } @@ -245,17 +119,12 @@ export async function createInstance(args: { * @param instanceId the id of the instance to delete */ export async function deleteInstance(projectId: string, instanceId: string): Promise { - const deleteRes = await api.request( - "DELETE", - `/${VERSION}/projects/${projectId}/instances/${instanceId}`, - { - auth: true, - origin: api.extensionsOrigin, - } + const deleteRes = await extensionsApiClient.delete<{ name: string }>( + `/projects/${projectId}/instances/${instanceId}`, ); const pollRes = await operationPoller.pollOperation({ - apiOrigin: api.extensionsOrigin, - apiVersion: VERSION, + apiOrigin: extensionsOrigin(), + apiVersion: EXTENSIONS_API_VERSION, operationResourceName: deleteRes.body.name, masterTimeout: 600000, }); @@ -266,25 +135,22 @@ export async function deleteInstance(projectId: string, instanceId: string): Pro * Get an instance by its id. * @param projectId the project where the instance exists * @param instanceId the id of the instance to delete - * @param options extra options to pass to api.request */ -export async function getInstance( - projectId: string, - instanceId: string, - options: any = {} -): Promise { - const res = await api.request( - "GET", - `/${VERSION}/projects/${projectId}/instances/${instanceId}`, - _.assign( - { - auth: true, - origin: api.extensionsOrigin, - }, - options - ) - ); - return res.body; +export async function getInstance(projectId: string, instanceId: string): Promise { + try { + const res = await extensionsApiClient.get(`/projects/${projectId}/instances/${instanceId}`); + return res.body; + } catch (err: any) { + if (err.status === 404) { + throw new FirebaseError( + `Extension instance '${clc.bold(instanceId)}' not found in project '${clc.bold( + projectId, + )}'.`, + { status: 404 }, + ); + } + throw err; + } } /** @@ -293,12 +159,13 @@ export async function getInstance( * @param projectId the project to list instances for */ export async function listInstances(projectId: string): Promise { - const instances: any[] = []; - const getNextPage = async (pageToken?: string) => { - const res = await api.request("GET", `/${VERSION}/projects/${projectId}/instances`, { - auth: true, - origin: api.extensionsOrigin, - query: { + const instances: ExtensionInstance[] = []; + const getNextPage = async (pageToken = ""): Promise => { + const res = await extensionsApiClient.get<{ + instances: ExtensionInstance[]; + nextPageToken?: string; + }>(`/projects/${projectId}/instances`, { + queryParams: { pageSize: PAGE_SIZE_MAX, pageToken, }, @@ -320,15 +187,21 @@ export async function listInstances(projectId: string): Promise; + systemParams?: Record; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; validateOnly?: boolean; }): Promise { - const res = await patchInstance({ + const reqBody: any = { projectId: args.projectId, instanceId: args.instanceId, updateMask: "config.params", @@ -338,8 +211,22 @@ export async function configureInstance(args: { params: args.params, }, }, - }); - return res; + }; + if (args.canEmitEvents) { + if (args.allowedEventTypes === undefined || args.eventarcChannel === undefined) { + throw new FirebaseError( + `This instance is configured to emit events, but either allowed event types or eventarc channel is undefined.`, + ); + } + reqBody.data.config.allowedEventTypes = args.allowedEventTypes; + reqBody.data.config.eventarcChannel = args.eventarcChannel; + } + reqBody.updateMask += ",config.allowed_event_types,config.eventarc_channel"; + if (args.systemParams) { + reqBody.data.config.systemParams = args.systemParams; + reqBody.updateMask += ",config.system_params"; + } + return patchInstance(reqBody); } /** @@ -349,13 +236,19 @@ export async function configureInstance(args: { * @param instanceId the id of the instance to configure * @param extensionSource the source for the version of the extension to update to * @param params params to configure the extension instance + * @param allowedEventTypes types of events (selected by consumer) that the extension is allowed to emit + * @param eventarcChannel fully qualified eventarc channel resource name to emit events to * @param validateOnly if true, only validates the update and makes no changes */ export async function updateInstance(args: { projectId: string; instanceId: string; extensionSource: ExtensionSource; - params?: { [option: string]: string }; + params?: Record; + systemParams?: Record; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; validateOnly?: boolean; }): Promise { const body: any = { @@ -368,7 +261,21 @@ export async function updateInstance(args: { body.config.params = args.params; updateMask += ",config.params"; } - return await patchInstance({ + if (args.systemParams) { + body.config.systemParams = args.systemParams; + updateMask += ",config.system_params"; + } + if (args.canEmitEvents) { + if (args.allowedEventTypes === undefined || args.eventarcChannel === undefined) { + throw new FirebaseError( + `This instance is configured to emit events, but either allowed event types or eventarc channel is undefined.`, + ); + } + body.config.allowedEventTypes = args.allowedEventTypes; + body.config.eventarcChannel = args.eventarcChannel; + } + updateMask += ",config.allowed_event_types,config.eventarc_channel"; + return patchInstance({ projectId: args.projectId, instanceId: args.instanceId, updateMask, @@ -384,13 +291,19 @@ export async function updateInstance(args: { * @param instanceId the id of the instance to configure * @param extRef reference for the extension to update to * @param params params to configure the extension instance + * @param allowedEventTypes types of events (selected by consumer) that the extension is allowed to emit + * @param eventarcChannel fully qualified eventarc channel resource name to emit events to * @param validateOnly if true, only validates the update and makes no changes */ export async function updateInstanceFromRegistry(args: { projectId: string; instanceId: string; extRef: string; - params?: { [option: string]: string }; + params?: Record; + systemParams?: Record; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; validateOnly?: boolean; }): Promise { const ref = refs.parse(args.extRef); @@ -405,7 +318,21 @@ export async function updateInstanceFromRegistry(args: { body.config.params = args.params; updateMask += ",config.params"; } - return await patchInstance({ + if (args.systemParams) { + body.config.systemParams = args.systemParams; + updateMask += ",config.system_params"; + } + if (args.canEmitEvents) { + if (args.allowedEventTypes === undefined || args.eventarcChannel === undefined) { + throw new FirebaseError( + `This instance is configured to emit events, but either allowed event types or eventarc channel is undefined.`, + ); + } + body.config.allowedEventTypes = args.allowedEventTypes; + body.config.eventarcChannel = args.eventarcChannel; + } + updateMask += ",config.allowed_event_types,config.eventarc_channel"; + return patchInstance({ projectId: args.projectId, instanceId: args.instanceId, updateMask, @@ -421,42 +348,42 @@ async function patchInstance(args: { validateOnly: boolean; data: any; }): Promise { - const updateRes = await api.request( - "PATCH", - `/${VERSION}/projects/${args.projectId}/instances/${args.instanceId}`, + const updateRes = await extensionsApiClient.patch( + `/projects/${args.projectId}/instances/${args.instanceId}`, + args.data, { - auth: true, - origin: api.extensionsOrigin, - query: { + queryParams: { updateMask: args.updateMask, - validateOnly: args.validateOnly, + validateOnly: args.validateOnly ? "true" : "false", }, - data: args.data, - } + }, ); if (args.validateOnly) { return updateRes; } const pollRes = await operationPoller.pollOperation({ - apiOrigin: api.extensionsOrigin, - apiVersion: VERSION, + apiOrigin: extensionsOrigin(), + apiVersion: EXTENSIONS_API_VERSION, operationResourceName: updateRes.body.name, masterTimeout: 600000, }); return pollRes; } -function populateResourceProperties(spec: ExtensionSpec): void { +export function populateSpec(spec: ExtensionSpec): void { if (spec) { - spec.resources.forEach((r) => { + for (const r of spec.resources) { try { if (r.propertiesYaml) { - r.properties = yaml.safeLoad(r.propertiesYaml); + r.properties = yaml.parse(r.propertiesYaml); } - } catch (err) { + } catch (err: any) { logger.debug(`[ext] failed to parse resource properties yaml: ${err}`); } - }); + } + // We need to populate empty repeated fields with empty arrays, since proto wire format removes them. + spec.params = spec.params ?? []; + spec.systemParams = spec.systemParams ?? []; } } @@ -470,44 +397,38 @@ function populateResourceProperties(spec: ExtensionSpec): void { export async function createSource( projectId: string, packageUri: string, - extensionRoot: string + extensionRoot: string, ): Promise { - const createRes = await api.request("POST", `/${VERSION}/projects/${projectId}/sources/`, { - auth: true, - origin: api.extensionsOrigin, - data: { - packageUri, - extensionRoot, - }, + const createRes = await extensionsApiClient.post< + { packageUri: string; extensionRoot: string }, + ExtensionSource + >(`/projects/${projectId}/sources/`, { + packageUri, + extensionRoot, }); const pollRes = await operationPoller.pollOperation({ - apiOrigin: api.extensionsOrigin, - apiVersion: VERSION, + apiOrigin: extensionsOrigin(), + apiVersion: EXTENSIONS_API_VERSION, operationResourceName: createRes.body.name, masterTimeout: 600000, }); if (pollRes.spec) { - populateResourceProperties(pollRes.spec); + populateSpec(pollRes.spec); } return pollRes; } -/** Get a extension source by its fully qualified path +/** + * Get a extension source by its fully qualified path * * @param sourceName the fully qualified path of the extension source (/projects//sources/) */ -export function getSource(sourceName: string): Promise { - return api - .request("GET", `/${VERSION}/${sourceName}`, { - auth: true, - origin: api.extensionsOrigin, - }) - .then((res) => { - if (res.body.spec) { - populateResourceProperties(res.body.spec); - } - return res.body; - }); +export async function getSource(sourceName: string): Promise { + const res = await extensionsApiClient.get(`/${sourceName}`); + if (res.body.spec) { + populateSpec(res.body.spec); + } + return res.body; } /** @@ -519,42 +440,40 @@ export async function getExtensionVersion(extensionVersionRef: string): Promise< throw new FirebaseError(`ExtensionVersion ref "${extensionVersionRef}" must supply a version.`); } try { - const res = await api.request("GET", `/${VERSION}/${refs.toExtensionVersionName(ref)}`, { - auth: true, - origin: api.extensionsOrigin, - }); + const res = await extensionsApiClient.get( + `/${refs.toExtensionVersionName(ref)}`, + ); if (res.body.spec) { - populateResourceProperties(res.body.spec); + populateSpec(res.body.spec); } return res.body; - } catch (err) { + } catch (err: any) { if (err.status === 404) { throw refNotFoundError(ref); } else if (err instanceof FirebaseError) { throw err; } throw new FirebaseError( - `Failed to query the extension version '${clc.bold(extensionVersionRef)}': ${err}` + `Failed to query the extension version '${clc.bold(extensionVersionRef)}': ${err}`, ); } } /** * @param publisherId the publisher for which we are listing Extensions - * @param showUnpublished whether to include unpublished Extensions, default = false */ export async function listExtensions(publisherId: string): Promise { const extensions: Extension[] = []; - const getNextPage = async (pageToken?: string) => { - const res = await api.request("GET", `/${VERSION}/publishers/${publisherId}/extensions`, { - auth: true, - origin: api.extensionsOrigin, - showUnpublished: false, - query: { - pageSize: PAGE_SIZE_MAX, - pageToken, + const getNextPage = async (pageToken = "") => { + const res = await extensionsApiClient.get<{ extensions: Extension[]; nextPageToken: string }>( + `/publishers/${publisherId}/extensions`, + { + queryParams: { + pageSize: PAGE_SIZE_MAX, + pageToken, + }, }, - }); + ); if (Array.isArray(res.body.extensions)) { extensions.push(...res.body.extensions); } @@ -568,28 +487,26 @@ export async function listExtensions(publisherId: string): Promise /** * @param ref user-friendly identifier for the ExtensionVersion (publisher-id/extension-id) - * @param showUnpublished whether to include unpublished ExtensionVersions, default = false */ export async function listExtensionVersions( ref: string, - filter?: string + filter = "", + showPrereleases = false, ): Promise { const { publisherId, extensionId } = refs.parse(ref); const extensionVersions: ExtensionVersion[] = []; - const getNextPage = async (pageToken?: string) => { - const res = await api.request( - "GET", - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions`, - { - auth: true, - origin: api.extensionsOrigin, - query: { - filter, - pageSize: PAGE_SIZE_MAX, - pageToken, - }, - } - ); + const getNextPage = async (pageToken = "") => { + const res = await extensionsApiClient.get<{ + extensionVersions: ExtensionVersion[]; + nextPageToken: string; + }>(`/publishers/${publisherId}/extensions/${extensionId}/versions`, { + queryParams: { + filter, + showPrereleases: String(showPrereleases), + pageSize: PAGE_SIZE_MAX, + pageToken, + }, + }); if (Array.isArray(res.body.extensionVersions)) { extensionVersions.push(...res.body.extensionVersions); } @@ -601,211 +518,6 @@ export async function listExtensionVersions( return extensionVersions; } -/** - * @param projectId the project for which we are registering a PublisherProfile - * @param publisherId the desired publisher ID - */ -export async function registerPublisherProfile( - projectId: string, - publisherId: string -): Promise { - const res = await api.request( - "POST", - `/${VERSION}/projects/${projectId}/publisherProfile:register`, - { - auth: true, - origin: api.extensionsOrigin, - data: { publisherId }, - } - ); - return res.body; -} - -/** - * @param extensionRef user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@version) - * @param deprecationMessage the deprecation message - */ -export async function deprecateExtensionVersion( - extensionRef: string, - deprecationMessage: string -): Promise { - const ref = refs.parse(extensionRef); - try { - const res = await api.request( - "POST", - `/${VERSION}/${refs.toExtensionVersionName(ref)}:deprecate`, - { - auth: true, - origin: api.extensionsOrigin, - data: { deprecationMessage }, - } - ); - return res.body; - } catch (err) { - if (err.status === 403) { - throw new FirebaseError( - `You are not the owner of extension '${clc.bold( - extensionRef - )}' and don’t have the correct permissions to deprecate this extension version.` + err, - { status: err.status } - ); - } else if (err.status === 404) { - throw new FirebaseError(`Extension version ${clc.bold(extensionRef)} was not found.`); - } else if (err instanceof FirebaseError) { - throw err; - } - throw new FirebaseError( - `Error occurred deprecating extension version '${extensionRef}': ${err}`, - { - status: err.status, - } - ); - } -} - -/** - * @param extensionRef user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@version) - */ -export async function undeprecateExtensionVersion(extensionRef: string): Promise { - const ref = refs.parse(extensionRef); - try { - const res = await api.request( - "POST", - `/${VERSION}/${refs.toExtensionVersionName(ref)}:undeprecate`, - { - auth: true, - origin: api.extensionsOrigin, - } - ); - return res.body; - } catch (err) { - if (err.status === 403) { - throw new FirebaseError( - `You are not the owner of extension '${clc.bold( - extensionRef - )}' and don’t have the correct permissions to undeprecate this extension version.`, - { status: err.status } - ); - } else if (err.status === 404) { - throw new FirebaseError(`Extension version ${clc.bold(extensionRef)} was not found.`); - } else if (err instanceof FirebaseError) { - throw err; - } - throw new FirebaseError( - `Error occurred undeprecating extension version '${extensionRef}': ${err}`, - { - status: err.status, - } - ); - } -} - -/** - * @param packageUri public URI of a zip or tarball of the extension source code - * @param extensionVersionRef user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@1.0.0) - * @param extensionRoot directory location of extension.yaml in the archived package, defaults to "/". - */ -export async function publishExtensionVersion( - extensionVersionRef: string, - packageUri: string, - extensionRoot?: string -): Promise { - const ref = refs.parse(extensionVersionRef); - if (!ref.version) { - throw new FirebaseError(`ExtensionVersion ref "${extensionVersionRef}" must supply a version.`); - } - - // TODO(b/185176470): Publishing an extension with a previously deleted name will return 409. - // Need to surface a better error, potentially by calling getExtension. - const publishRes = await api.request( - "POST", - `/${VERSION}/${refs.toExtensionName(ref)}/versions:publish`, - { - auth: true, - origin: api.extensionsOrigin, - data: { - versionId: ref.version, - packageUri, - extensionRoot: extensionRoot ?? "/", - }, - } - ); - const pollRes = await operationPoller.pollOperation({ - apiOrigin: api.extensionsOrigin, - apiVersion: VERSION, - operationResourceName: publishRes.body.name, - masterTimeout: 600000, - }); - return pollRes; -} - -/** - * @deprecated This endpoint is replaced with deleteExtension. - * @param extensionRef user-friendly identifier for the Extension (publisher-id/extension-id) - */ -export async function unpublishExtension(extensionRef: string): Promise { - const ref = refs.parse(extensionRef); - if (ref.version) { - throw new FirebaseError(`Extension reference "${extensionRef}" must not contain a version.`); - } - const url = `/${VERSION}/${refs.toExtensionName(ref)}:unpublish`; - try { - await api.request("POST", url, { - auth: true, - origin: api.extensionsOrigin, - }); - } catch (err) { - if (err.status === 403) { - throw new FirebaseError( - `You are not the owner of extension '${clc.bold( - extensionRef - )}' and don’t have the correct permissions to unpublish this extension.`, - { status: err.status } - ); - } else if (err instanceof FirebaseError) { - throw err; - } - throw new FirebaseError(`Error occurred unpublishing extension '${extensionRef}': ${err}`, { - status: err.status, - }); - } -} - -/** - * Delete a published extension. - * This will also mark the name as reserved to prevent future usages. - * @param extensionRef user-friendly identifier for the Extension (publisher-id/extension-id) - */ -export async function deleteExtension(extensionRef: string): Promise { - const ref = refs.parse(extensionRef); - if (ref.version) { - throw new FirebaseError(`Extension reference "${extensionRef}" must not contain a version.`); - } - const url = `/${VERSION}/${refs.toExtensionName(ref)}`; - try { - await api.request("DELETE", url, { - auth: true, - origin: api.extensionsOrigin, - }); - } catch (err) { - if (err.status === 403) { - throw new FirebaseError( - `You are not the owner of extension '${clc.bold( - extensionRef - )}' and don’t have the correct permissions to delete this extension.`, - { status: err.status } - ); - } else if (err.status === 404) { - throw new FirebaseError(`Extension ${clc.bold(extensionRef)} was not found.`); - } else if (err instanceof FirebaseError) { - throw err; - } - throw new FirebaseError(`Error occurred delete extension '${extensionRef}': ${err}`, { - status: err.status, - }); - } -} - /** * @param ref user-friendly identifier for the Extension (publisher-id/extension-id) * @return the extension @@ -813,12 +525,9 @@ export async function deleteExtension(extensionRef: string): Promise { export async function getExtension(extensionRef: string): Promise { const ref = refs.parse(extensionRef); try { - const res = await api.request("GET", `/${VERSION}/${refs.toExtensionName(ref)}`, { - auth: true, - origin: api.extensionsOrigin, - }); + const res = await extensionsApiClient.get(`/${refs.toExtensionName(ref)}`); return res.body; - } catch (err) { + } catch (err: any) { if (err.status === 404) { throw refNotFoundError(ref); } else if (err instanceof FirebaseError) { @@ -830,22 +539,18 @@ export async function getExtension(extensionRef: string): Promise { } } -function refNotFoundError(ref: refs.Ref): FirebaseError { +export function refNotFoundError(ref: refs.Ref): FirebaseError { return new FirebaseError( `The extension reference '${clc.bold( - ref.version ? refs.toExtensionVersionRef(ref) : refs.toExtensionRef(ref) + ref.version ? refs.toExtensionVersionRef(ref) : refs.toExtensionRef(ref), )}' doesn't exist. This could happen for two reasons:\n` + ` -The publisher ID '${clc.bold(ref.publisherId)}' doesn't exist or could be misspelled\n` + ` -The name of the ${ref.version ? "extension version" : "extension"} '${clc.bold( - ref.version ? `${ref.extensionId}@${ref.version}` : ref.extensionId + ref.version ? `${ref.extensionId}@${ref.version}` : ref.extensionId, )}' doesn't exist or could be misspelled\n\n` + `Please correct the extension reference and try again. If you meant to install an extension from a local source, please provide a relative path prefixed with '${clc.bold( - "./" - )}', '${clc.bold("../")}', or '${clc.bold( - "~/" - )}'. Learn more about local extension installation at ${marked( - "[https://firebase.google.com/docs/extensions/alpha/install-extensions_community#install](https://firebase.google.com/docs/extensions/alpha/install-extensions_community#install)." - )}`, - { status: 404 } + "./", + )}', '${clc.bold("../")}', or '${clc.bold("~/")}'.}`, + { status: 404 }, ); } diff --git a/src/extensions/extensionsHelper.spec.ts b/src/extensions/extensionsHelper.spec.ts new file mode 100644 index 00000000000..697a2fc5bfb --- /dev/null +++ b/src/extensions/extensionsHelper.spec.ts @@ -0,0 +1,1005 @@ +import * as clc from "colorette"; +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { FirebaseError } from "../error"; +import * as extensionsApi from "./extensionsApi"; +import * as publisherApi from "./publisherApi"; +import * as extensionsHelper from "./extensionsHelper"; +import * as getProjectNumber from "../getProjectNumber"; +import * as functionsConfig from "../functionsConfig"; +import { storage } from "../gcp"; +import * as archiveDirectory from "../archiveDirectory"; +import * as prompt from "../prompt"; +import { + ExtensionSource, + ExtensionSpec, + Param, + ParamType, + Extension, + Visibility, + RegistryLaunchStage, +} from "./types"; +import { Readable } from "stream"; +import { ArchiveResult } from "../archiveDirectory"; + +describe("extensionsHelper", () => { + describe("substituteParams", () => { + it("should substitute env variables", () => { + const testResources = [ + { + resourceOne: { + name: "${VAR_ONE}", + source: "path/${VAR_ONE}", + }, + }, + { + resourceTwo: { + property: "${VAR_TWO}", + another: "$NOT_ENV", + }, + }, + ]; + const testParam = { VAR_ONE: "foo", VAR_TWO: "bar", UNUSED: "faz" }; + expect(extensionsHelper.substituteParams(testResources, testParam)).to.deep.equal([ + { + resourceOne: { + name: "foo", + source: "path/foo", + }, + }, + { + resourceTwo: { + property: "bar", + another: "$NOT_ENV", + }, + }, + ]); + }); + }); + + it("should support both ${PARAM_NAME} AND ${param:PARAM_NAME} syntax", () => { + const testResources = [ + { + resourceOne: { + name: "${param:VAR_ONE}", + source: "path/${param:VAR_ONE}", + }, + }, + { + resourceTwo: { + property: "${param:VAR_TWO}", + another: "$NOT_ENV", + }, + }, + { + resourceThree: { + property: "${VAR_TWO}${VAR_TWO}${param:VAR_TWO}", + another: "${not:VAR_TWO}", + }, + }, + ]; + const testParam = { VAR_ONE: "foo", VAR_TWO: "bar", UNUSED: "faz" }; + expect(extensionsHelper.substituteParams(testResources, testParam)).to.deep.equal([ + { + resourceOne: { + name: "foo", + source: "path/foo", + }, + }, + { + resourceTwo: { + property: "bar", + another: "$NOT_ENV", + }, + }, + { + resourceThree: { + property: "barbarbar", + another: "${not:VAR_TWO}", + }, + }, + ]); + }); + + describe("getDBInstanceFromURL", () => { + it("returns the correct instance name", () => { + expect(extensionsHelper.getDBInstanceFromURL("https://my-db.firebaseio.com")).to.equal( + "my-db", + ); + }); + }); + + describe("populateDefaultParams", () => { + const expected = { + ENV_VAR_ONE: "12345", + ENV_VAR_TWO: "hello@example.com", + ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", + }; + + const exampleParamSpec: Param[] = [ + { + param: "ENV_VAR_ONE", + label: "env1", + required: true, + }, + { + param: "ENV_VAR_TWO", + label: "env2", + required: true, + validationRegex: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", + validationErrorMessage: "You must provide a valid email address.\n", + }, + { + param: "ENV_VAR_THREE", + label: "env3", + default: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", + validationRegex: ".*\\{token\\}.*", + validationErrorMessage: + "Your URL must include {token} so that it can be replaced with an actual invitation token.\n", + }, + { + param: "ENV_VAR_FOUR", + label: "env4", + default: "users/{sender}.friends", + required: false, + validationRegex: ".+/.+\\..+", + validationErrorMessage: + "Values must be comma-separated document path + field, e.g. coll/doc.field,coll/doc.field\n", + }, + ]; + + it("should set default if default is available", () => { + const envFile = { + ENV_VAR_ONE: "12345", + ENV_VAR_TWO: "hello@example.com", + ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", + }; + + expect(extensionsHelper.populateDefaultParams(envFile, exampleParamSpec)).to.deep.equal( + expected, + ); + }); + + it("should throw error if no default is available", () => { + const envFile = { + ENV_VAR_ONE: "12345", + ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", + ENV_VAR_FOUR: "users/{sender}.friends", + }; + + expect(() => { + extensionsHelper.populateDefaultParams(envFile, exampleParamSpec); + }).to.throw(FirebaseError, /no default available/); + }); + }); + + describe("validateCommandLineParams", () => { + const exampleParamSpec: Param[] = [ + { + param: "ENV_VAR_ONE", + label: "env1", + required: true, + }, + { + param: "ENV_VAR_TWO", + label: "env2", + required: true, + validationRegex: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", + validationErrorMessage: "You must provide a valid email address.\n", + }, + { + param: "ENV_VAR_THREE", + label: "env3", + default: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", + validationRegex: ".*\\{token\\}.*", + validationErrorMessage: + "Your URL must include {token} so that it can be replaced with an actual invitation token.\n", + }, + { + param: "ENV_VAR_FOUR", + label: "env3", + default: "users/{sender}.friends", + required: false, + validationRegex: ".+/.+\\..+", + validationErrorMessage: + "Values must be comma-separated document path + field, e.g. coll/doc.field,coll/doc.field\n", + }, + ]; + + it("should throw error if param variable value is invalid", () => { + const envFile = { + ENV_VAR_ONE: "12345", + ENV_VAR_TWO: "invalid", + ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", + ENV_VAR_FOUR: "users/{sender}.friends", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(envFile, exampleParamSpec); + }).to.throw(FirebaseError, /not valid/); + }); + + it("should throw error if # commandLineParams does not match # env vars from extension.yaml", () => { + const envFile = { + ENV_VAR_ONE: "12345", + ENV_VAR_TWO: "invalid", + ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(envFile, exampleParamSpec); + }).to.throw(FirebaseError); + }); + + it("should throw an error if a required param is missing", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + required: true, + }, + { + param: "BYE", + label: "goodbye", + required: false, + }, + ]; + const testParams = { + BYE: "val", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).to.throw(FirebaseError); + }); + + it("should not throw a error if a non-required param is missing", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + required: true, + }, + { + param: "BYE", + label: "goodbye", + required: false, + }, + ]; + const testParams = { + HI: "val", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).not.to.throw(); + }); + + it("should not throw a regex error if a non-required param is missing", () => { + const testParamSpec = [ + { + param: "BYE", + label: "goodbye", + required: false, + validationRegex: "FAIL", + }, + ]; + const testParams = {}; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).not.to.throw(); + }); + + it("should throw a error if a param value doesn't pass the validation regex", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + validationRegex: "FAIL", + required: true, + }, + ]; + const testParams = { + HI: "val", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).to.throw(FirebaseError); + }); + + it("should throw a error if a multiselect value isn't an option", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + type: ParamType.MULTISELECT, + options: [ + { + value: "val", + }, + ], + required: true, + }, + ]; + const testParams = { + HI: "val,FAIL", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).to.throw(FirebaseError); + }); + + it("should throw a error if a multiselect param is missing options", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + type: ParamType.MULTISELECT, + options: [], + validationRegex: "FAIL", + required: true, + }, + ]; + const testParams = { + HI: "FAIL,val", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).to.throw(FirebaseError); + }); + + it("should throw a error if a select param is missing options", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + type: ParamType.SELECT, + validationRegex: "FAIL", + options: [], + required: true, + }, + ]; + const testParams = { + HI: "FAIL,val", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).to.throw(FirebaseError); + }); + + it("should not throw if a select value is an option", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + type: ParamType.SELECT, + options: [ + { + value: "val", + }, + ], + required: true, + }, + ]; + const testParams = { + HI: "val", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).not.to.throw(); + }); + + it("should not throw if all multiselect values are options", () => { + const testParamSpec = [ + { + param: "HI", + label: "hello", + type: ParamType.MULTISELECT, + options: [ + { + value: "val", + }, + { + value: "val2", + }, + ], + required: true, + }, + ]; + const testParams = { + HI: "val,val2", + }; + + expect(() => { + extensionsHelper.validateCommandLineParams(testParams, testParamSpec); + }).not.to.throw(); + }); + }); + + describe("validateSpec", () => { + it("should not error on a valid spec", () => { + const testSpec: ExtensionSpec = { + name: "test", + version: "0.1.0", + specVersion: "v1beta", + resources: [], + params: [], + systemParams: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).not.to.throw(); + }); + it("should error if license is missing", () => { + const testSpec: ExtensionSpec = { + name: "test", + version: "0.1.0", + specVersion: "v1beta", + resources: [], + params: [], + systemParams: [], + sourceUrl: "https://test-source.fake", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /license/); + }); + it("should error if license is invalid", () => { + const testSpec: ExtensionSpec = { + name: "test", + version: "0.1.0", + specVersion: "v1beta", + resources: [], + params: [], + systemParams: [], + sourceUrl: "https://test-source.fake", + license: "invalid-license", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /license/); + }); + it("should error if name is missing", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /name/); + }); + + it("should error if specVersion is missing", () => { + const testSpec = { + name: "test", + version: "0.1.0", + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /specVersion/); + }); + + it("should error if version is missing", () => { + const testSpec = { + name: "test", + specVersion: "v1beta", + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /version/); + }); + + it("should error if a resource is malformed", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + resources: [{}], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /name/); + }); + + it("should error if an api is malformed", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + apis: [{}], + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /apiName/); + }); + + it("should error if a param is malformed", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + params: [{}], + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /param/); + }); + + it("should error if a STRING param has options.", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + params: [{ options: [] }], + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /options/); + }); + + it("should error if a select param has validationRegex.", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + params: [{ type: extensionsHelper.SpecParamType.SELECT, validationRegex: "test" }], + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /validationRegex/); + }); + it("should error if a param has an invalid type.", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + params: [{ type: "test-type", validationRegex: "test" }], + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /Invalid type/); + }); + it("should error if a param selectResource missing resourceType.", () => { + const testSpec = { + version: "0.1.0", + specVersion: "v1beta", + params: [ + { + type: extensionsHelper.SpecParamType.SELECTRESOURCE, + validationRegex: "test", + default: "fail", + }, + ], + resources: [], + sourceUrl: "https://test-source.fake", + license: "apache-2.0", + }; + + expect(() => { + extensionsHelper.validateSpec(testSpec); + }).to.throw(FirebaseError, /must have resourceType/); + }); + }); + + describe("promptForValidInstanceId", () => { + let promptStub: sinon.SinonStub; + + beforeEach(() => { + promptStub = sinon.stub(prompt, "promptOnce"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should prompt the user and return if the user provides a valid id", async () => { + const extensionName = "extension-name"; + const userInput = "a-valid-name"; + promptStub.returns(userInput); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput); + expect(promptStub).to.have.been.calledOnce; + }); + + it("should prompt the user again if the provided id is shorter than 6 characters", async () => { + const extensionName = "extension-name"; + const userInput1 = "short"; + const userInput2 = "a-valid-name"; + promptStub.onCall(0).returns(userInput1); + promptStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(promptStub).to.have.been.calledTwice; + }); + + it("should prompt the user again if the provided id is longer than 45 characters", async () => { + const extensionName = "extension-name"; + const userInput1 = "a-really-long-name-that-is-really-longer-than-were-ok-with"; + const userInput2 = "a-valid-name"; + promptStub.onCall(0).returns(userInput1); + promptStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(promptStub).to.have.been.calledTwice; + }); + + it("should prompt the user again if the provided id ends in a -", async () => { + const extensionName = "extension-name"; + const userInput1 = "invalid-"; + const userInput2 = "-invalid"; + const userInput3 = "a-valid-name"; + promptStub.onCall(0).returns(userInput1); + promptStub.onCall(1).returns(userInput2); + promptStub.onCall(2).returns(userInput3); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput3); + expect(promptStub).to.have.been.calledThrice; + }); + + it("should prompt the user again if the provided id starts with a number", async () => { + const extensionName = "extension-name"; + const userInput1 = "1invalid"; + const userInput2 = "a-valid-name"; + promptStub.onCall(0).returns(userInput1); + promptStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(promptStub).to.have.been.calledTwice; + }); + + it("should prompt the user again if the provided id contains illegal characters", async () => { + const extensionName = "extension-name"; + const userInput1 = "na.name@name"; + const userInput2 = "a-valid-name"; + promptStub.onCall(0).returns(userInput1); + promptStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(promptStub).to.have.been.calledTwice; + }); + }); + + describe("createSourceFromLocation", () => { + let archiveStub: sinon.SinonStub; + let uploadStub: sinon.SinonStub; + let createSourceStub: sinon.SinonStub; + let deleteStub: sinon.SinonStub; + const testUrl = "https://storage.googleapis.com/firebase-ext-eap-uploads/object.zip"; + const testSource: ExtensionSource = { + name: "test", + packageUri: testUrl, + hash: "abc123", + state: "ACTIVE", + spec: { + name: "projects/test-proj/sources/abc123", + version: "0.0.0", + sourceUrl: testUrl, + resources: [], + params: [], + systemParams: [], + }, + }; + const testArchivedFiles: ArchiveResult = { + file: "somefile", + manifest: ["file"], + size: 4, + source: "/some/path", + stream: new Readable(), + }; + const testUploadedArchive: { bucket: string; object: string; generation: string | null } = { + bucket: extensionsHelper.EXTENSIONS_BUCKET_NAME, + object: "object.zip", + generation: "1", + }; + + beforeEach(() => { + archiveStub = sinon.stub(archiveDirectory, "archiveDirectory").resolves(testArchivedFiles); + uploadStub = sinon.stub(storage, "uploadObject").resolves(testUploadedArchive); + createSourceStub = sinon.stub(extensionsApi, "createSource").resolves(testSource); + deleteStub = sinon.stub(storage, "deleteObject").resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should upload local sources to Firebase Storage then create an ExtensionSource", async () => { + const result = await extensionsHelper.createSourceFromLocation("test-proj", "."); + + expect(result).to.equal(testSource); + expect(archiveStub).to.have.been.calledWith("."); + expect(uploadStub).to.have.been.calledWith( + testArchivedFiles, + extensionsHelper.EXTENSIONS_BUCKET_NAME, + ); + expect(createSourceStub).to.have.been.calledWith("test-proj", testUrl + "?alt=media", "/"); + expect(deleteStub).to.have.been.calledWith( + `/${extensionsHelper.EXTENSIONS_BUCKET_NAME}/object.zip`, + ); + }); + + it("should succeed even when it fails to delete the uploaded archive", async () => { + deleteStub.throws(); + + const result = await extensionsHelper.createSourceFromLocation("test-proj", "."); + + expect(result).to.equal(testSource); + expect(archiveStub).to.have.been.calledWith("."); + expect(uploadStub).to.have.been.calledWith( + testArchivedFiles, + extensionsHelper.EXTENSIONS_BUCKET_NAME, + ); + expect(createSourceStub).to.have.been.calledWith("test-proj", testUrl + "?alt=media", "/"); + expect(deleteStub).to.have.been.calledWith( + `/${extensionsHelper.EXTENSIONS_BUCKET_NAME}/object.zip`, + ); + }); + + it("should throw an error if one is thrown while uploading a local source", async () => { + uploadStub.throws(new FirebaseError("something bad happened")); + + await expect(extensionsHelper.createSourceFromLocation("test-proj", ".")).to.be.rejectedWith( + FirebaseError, + ); + + expect(archiveStub).to.have.been.calledWith("."); + expect(uploadStub).to.have.been.calledWith( + testArchivedFiles, + extensionsHelper.EXTENSIONS_BUCKET_NAME, + ); + expect(createSourceStub).not.to.have.been.called; + expect(deleteStub).not.to.have.been.called; + }); + }); + + describe("checkIfInstanceIdAlreadyExists", () => { + const TEST_NAME = "image-resizer"; + let getInstanceStub: sinon.SinonStub; + + beforeEach(() => { + getInstanceStub = sinon.stub(extensionsApi, "getInstance"); + }); + + afterEach(() => { + getInstanceStub.restore(); + }); + + it("should return false if no instance with that name exists", async () => { + getInstanceStub.throws(new FirebaseError("Not Found", { status: 404 })); + + const exists = await extensionsHelper.instanceIdExists("proj", TEST_NAME); + expect(exists).to.be.false; + }); + + it("should return true if an instance with that name exists", async () => { + getInstanceStub.resolves({ name: TEST_NAME }); + + const exists = await extensionsHelper.instanceIdExists("proj", TEST_NAME); + expect(exists).to.be.true; + }); + + it("should throw if it gets an unexpected error response from getInstance", async () => { + getInstanceStub.throws(new FirebaseError("Internal Error", { status: 500 })); + + await expect(extensionsHelper.instanceIdExists("proj", TEST_NAME)).to.be.rejectedWith( + FirebaseError, + "Unexpected error when checking if instance ID exists: FirebaseError: Internal Error", + ); + }); + }); + + describe("getFirebaseProjectParams", () => { + const sandbox = sinon.createSandbox(); + let projectNumberStub: sinon.SinonStub; + let getFirebaseConfigStub: sinon.SinonStub; + + beforeEach(() => { + projectNumberStub = sandbox.stub(getProjectNumber, "getProjectNumber").resolves("1"); + getFirebaseConfigStub = sandbox.stub(functionsConfig, "getFirebaseConfig").resolves({ + projectId: "test", + storageBucket: "real-test.appspot.com", + databaseURL: "https://real-test.firebaseio.com", + locationId: "us-west1", + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should not call prodution when using a demo- project in emulator mode", async () => { + const res = await extensionsHelper.getFirebaseProjectParams("demo-test", true); + + expect(res).to.deep.equal({ + DATABASE_INSTANCE: "demo-test", + DATABASE_URL: "https://demo-test.firebaseio.com", + FIREBASE_CONFIG: + '{"projectId":"demo-test","databaseURL":"https://demo-test.firebaseio.com","storageBucket":"demo-test.appspot.com"}', + PROJECT_ID: "demo-test", + PROJECT_NUMBER: "0", + STORAGE_BUCKET: "demo-test.appspot.com", + }); + expect(projectNumberStub).not.to.have.been.called; + expect(getFirebaseConfigStub).not.to.have.been.called; + }); + + it("should return real values for non 'demo-' projects", async () => { + const res = await extensionsHelper.getFirebaseProjectParams("real-test", false); + + expect(res).to.deep.equal({ + DATABASE_INSTANCE: "real-test", + DATABASE_URL: "https://real-test.firebaseio.com", + FIREBASE_CONFIG: + '{"projectId":"real-test","databaseURL":"https://real-test.firebaseio.com","storageBucket":"real-test.appspot.com"}', + PROJECT_ID: "real-test", + PROJECT_NUMBER: "1", + STORAGE_BUCKET: "real-test.appspot.com", + }); + expect(projectNumberStub).to.have.been.called; + expect(getFirebaseConfigStub).to.have.been.called; + }); + }); + + describe("getNextVersionByStage", () => { + let listExtensionVersionsStub: sinon.SinonStub; + + beforeEach(() => { + listExtensionVersionsStub = sinon.stub(publisherApi, "listExtensionVersions"); + }); + + afterEach(() => { + listExtensionVersionsStub.restore(); + }); + + it("should return expected stages and versions", async () => { + listExtensionVersionsStub.returns( + Promise.resolve([ + { spec: { version: "1.0.0-rc.0" } }, + { spec: { version: "1.0.0-rc.1" } }, + { spec: { version: "1.0.0-beta.0" } }, + ]), + ); + const expected = new Map([ + ["rc", "1.0.0-rc.2"], + ["alpha", "1.0.0-alpha.0"], + ["beta", "1.0.0-beta.1"], + ["stable", "1.0.0"], + ]); + const { versionByStage, hasVersions } = await extensionsHelper.getNextVersionByStage( + "test", + "1.0.0", + ); + expect(Array.from(versionByStage.entries())).to.eql(Array.from(expected.entries())); + expect(hasVersions).to.eql(true); + }); + + it("should ignore unknown stages and different prerelease format", async () => { + listExtensionVersionsStub.returns( + Promise.resolve([ + { spec: { version: "1.0.0-beta" } }, + { spec: { version: "1.0.0-prealpha.0" } }, + ]), + ); + const expected = new Map([ + ["rc", "1.0.0-rc.0"], + ["alpha", "1.0.0-alpha.0"], + ["beta", "1.0.0-beta.0"], + ["stable", "1.0.0"], + ]); + const { versionByStage, hasVersions } = await extensionsHelper.getNextVersionByStage( + "test", + "1.0.0", + ); + expect(Array.from(versionByStage.entries())).to.eql(Array.from(expected.entries())); + expect(hasVersions).to.eql(true); + }); + }); + + describe("unpackExtensionState", () => { + const testExtension: Extension = { + name: "publishers/publisher-id/extensions/extension-id", + ref: "publisher-id/extension-id", + visibility: Visibility.PUBLIC, + registryLaunchStage: RegistryLaunchStage.BETA, + createTime: "", + state: "PUBLISHED", + }; + it("should return correct published state", () => { + expect( + extensionsHelper.unpackExtensionState({ + ...testExtension, + state: "PUBLISHED", + latestVersion: "1.0.0", + latestApprovedVersion: "1.0.0", + }), + ).to.eql(clc.bold(clc.green("Published"))); + }); + it("should return correct uploaded state", () => { + expect( + extensionsHelper.unpackExtensionState({ + ...testExtension, + state: "PUBLISHED", + latestVersion: "1.0.0", + }), + ).to.eql(clc.green("Uploaded")); + }); + it("should return correct deprecated state", () => { + expect( + extensionsHelper.unpackExtensionState({ + ...testExtension, + state: "DEPRECATED", + }), + ).to.eql(clc.red("Deprecated")); + }); + it("should return correct suspended state", () => { + expect( + extensionsHelper.unpackExtensionState({ + ...testExtension, + state: "SUSPENDED", + latestVersion: "1.0.0", + }), + ).to.eql(clc.bold(clc.red("Suspended"))); + }); + it("should return correct prerelease state", () => { + expect( + extensionsHelper.unpackExtensionState({ + ...testExtension, + state: "PUBLISHED", + }), + ).to.eql("Prerelease"); + }); + }); +}); diff --git a/src/extensions/extensionsHelper.ts b/src/extensions/extensionsHelper.ts index 47c24b0e602..a99489d7536 100644 --- a/src/extensions/extensionsHelper.ts +++ b/src/extensions/extensionsHelper.ts @@ -1,41 +1,47 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as ora from "ora"; import * as semver from "semver"; -import * as marked from "marked"; +import * as tmp from "tmp"; +import * as fs from "fs-extra"; +import fetch from "node-fetch"; +import * as path from "path"; +import { marked } from "marked"; +import { createUnzipTransform } from "./../unzip"; const TerminalRenderer = require("marked-terminal"); marked.setOptions({ renderer: new TerminalRenderer(), }); -import { storageOrigin } from "../api"; +import { extensionsOrigin, extensionsPublisherOrigin, storageOrigin } from "../api"; import { archiveDirectory } from "../archiveDirectory"; import { convertOfficialExtensionsToList } from "./utils"; import { getFirebaseConfig } from "../functionsConfig"; -import { getExtensionRegistry, resolveSourceUrl, resolveRegistryEntry } from "./resolveSource"; +import { getProjectAdminSdkConfigOrCached } from "../emulator/adminSdkConfig"; +import { getExtensionRegistry } from "./resolveSource"; import { FirebaseError } from "../error"; +import { diagnose } from "./diagnose"; import { checkResponse } from "./askUserForParam"; import { ensure } from "../ensureApiEnabled"; import { deleteObject, uploadObject } from "../gcp/storage"; -import { needProjectId } from "../projectUtils"; +import { getProjectId } from "../projectUtils"; +import { createSource, getInstance } from "./extensionsApi"; import { - createSource, - ExtensionSource, - ExtensionVersion, + createExtensionVersionFromGitHubSource, + createExtensionVersionFromLocalSource, getExtension, - getInstance, - getSource, - Param, - publishExtensionVersion, -} from "./extensionsApi"; + getExtensionVersion, + listExtensionVersions, +} from "./publisherApi"; +import { Extension, ExtensionSource, ExtensionSpec, ExtensionVersion, Param } from "./types"; import * as refs from "./refs"; -import { getLocalExtensionSpec } from "./localHelper"; -import { promptOnce } from "../prompt"; +import { EXTENSIONS_SPEC_FILE, readFile, getLocalExtensionSpec } from "./localHelper"; +import { confirm, promptOnce } from "../prompt"; import { logger } from "../logger"; import { envOverride } from "../utils"; -import { getLocalChangelog } from "./changelog"; +import { getLocalChangelog } from "./change-log"; import { getProjectNumber } from "../getProjectNumber"; +import { Constants } from "../emulator/constants"; /** * SpecParamType represents the exact strings that the extensions @@ -66,7 +72,7 @@ const VALID_LICENSES = ["apache-2.0"]; export const URL_REGEX = /^https:/; export const EXTENSIONS_BUCKET_NAME = envOverride( "FIREBASE_EXTENSIONS_UPLOAD_BUCKET", - "firebase-ext-eap-uploads" + "firebase-ext-eap-uploads", ); const AUTOPOPULATED_PARAM_NAMES = [ "PROJECT_ID", @@ -86,6 +92,9 @@ export const AUTOPOULATED_PARAM_PLACEHOLDERS = { export const resourceTypeToNiceName: Record = { "firebaseextensions.v1beta.function": "Cloud Function", }; +export type ReleaseStage = "alpha" | "beta" | "rc" | "stable"; +const repoRegex = new RegExp(`^https:\/\/github\.com\/[^\/]+\/[^\/]+$`); +const stageOptions = ["rc", "alpha", "beta", "stable"]; /** * Turns database URLs (e.g. https://my-db.firebaseio.com) into database instance names @@ -94,7 +103,7 @@ export const resourceTypeToNiceName: Record = { */ export function getDBInstanceFromURL(databaseUrl = ""): string { const instanceRegex = new RegExp("(?:https://)(.*)(?:.firebaseio.com)"); - const matches = databaseUrl.match(instanceRegex); + const matches = instanceRegex.exec(databaseUrl); if (matches && matches.length > 1) { return matches[1]; } @@ -104,23 +113,36 @@ export function getDBInstanceFromURL(databaseUrl = ""): string { /** * Gets Firebase project specific param values. */ -export async function getFirebaseProjectParams(projectId: string): Promise> { - const body = await getFirebaseConfig({ project: projectId }); - const projectNumber = await getProjectNumber({ projectId }); +export async function getFirebaseProjectParams( + projectId: string | undefined, + emulatorMode: boolean = false, +): Promise> { + if (!projectId) { + return {}; + } + const body = emulatorMode + ? await getProjectAdminSdkConfigOrCached(projectId) + : await getFirebaseConfig({ project: projectId }); + const projectNumber = + emulatorMode && Constants.isDemoProject(projectId) + ? Constants.FAKE_PROJECT_NUMBER + : await getProjectNumber({ projectId }); + const databaseURL = body?.databaseURL ?? `https://${projectId}.firebaseio.com`; + const storageBucket = body?.storageBucket ?? `${projectId}.appspot.com`; // This env variable is needed for parameter-less initialization of firebase-admin const FIREBASE_CONFIG = JSON.stringify({ - projectId: body.projectId, - databaseURL: body.databaseURL, - storageBucket: body.storageBucket, + projectId, + databaseURL, + storageBucket, }); return { - PROJECT_ID: body.projectId, + PROJECT_ID: projectId, PROJECT_NUMBER: projectNumber, - DATABASE_URL: body.databaseURL, - STORAGE_BUCKET: body.storageBucket, + DATABASE_URL: databaseURL, + STORAGE_BUCKET: storageBucket, FIREBASE_CONFIG, - DATABASE_INSTANCE: getDBInstanceFromURL(body.databaseURL), + DATABASE_INSTANCE: getDBInstanceFromURL(databaseURL), }; } @@ -141,9 +163,13 @@ export function substituteParams(original: T, params: Record) const substituteRegexMatches = (unsubstituted: string, regex: RegExp): string => { return unsubstituted.replace(regex, paramVal); }; - return _.reduce(regexes, substituteRegexMatches, str); + return regexes.reduce(substituteRegexMatches, str); }; - return JSON.parse(_.reduce(params, applySubstitution, startingString)); + const s = Object.entries(params).reduce( + (str, [key, val]) => applySubstitution(str, val, key), + startingString, + ); + return JSON.parse(s); } /** @@ -153,17 +179,20 @@ export function substituteParams(original: T, params: Record) * @param paramSpec information on params parsed from extension.yaml * @return JSON object of params */ -export function populateDefaultParams(paramVars: Record, paramSpecs: Param[]): any { +export function populateDefaultParams( + paramVars: Record, + paramSpecs: Param[], +): Record { const newParams = paramVars; for (const param of paramSpecs) { if (!paramVars[param.param]) { - if (param.default != undefined && param.required) { + if (param.default !== undefined && param.required) { newParams[param.param] = param.default; } else if (param.required) { throw new FirebaseError( `${param.param} has not been set in the given params file` + - " and there is no default available. Please set this variable before installing again." + " and there is no default available. Please set this variable before installing again.", ); } } @@ -179,7 +208,7 @@ export function populateDefaultParams(paramVars: Record, paramSp */ export function validateCommandLineParams( envVars: Record, - paramSpec: Param[] + paramSpec: Param[], ): void { const paramNames = paramSpec.map((p) => p.param); const misnamedParams = Object.keys(envVars).filter((key: string) => { @@ -188,7 +217,7 @@ export function validateCommandLineParams( if (misnamedParams.length) { logger.warn( "Warning: The following params were specified in your env file but do not exist in the extension spec: " + - `${misnamedParams.join(", ")}.` + `${misnamedParams.join(", ")}.`, ); } let allParamsValid = true; @@ -218,6 +247,15 @@ export function validateSpec(spec: any) { } if (!spec.version) { errors.push("extension.yaml is missing required field: version"); + } else if (!semver.valid(spec.version)) { + errors.push(`version ${spec.version} in extension.yaml is not a valid semver`); + } else { + const version = semver.parse(spec.version)!; + if (version.prerelease.length > 0 || version.build.length > 0) { + errors.push( + "version field in extension.yaml does not support pre-release annotations; instead, set a pre-release stage using the --stage flag", + ); + } } if (!spec.license) { errors.push("extension.yaml is missing required field: license"); @@ -225,7 +263,7 @@ export function validateSpec(spec: any) { const formattedLicense = String(spec.license).toLocaleLowerCase(); if (!VALID_LICENSES.includes(formattedLicense)) { errors.push( - `license field in extension.yaml is invalid. Valid value(s): ${VALID_LICENSES.join(", ")}` + `license field in extension.yaml is invalid. Valid value(s): ${VALID_LICENSES.join(", ")}`, ); } } @@ -238,7 +276,7 @@ export function validateSpec(spec: any) { } if (!resource.type) { errors.push( - `Resource${resource.name ? ` ${resource.name}` : ""} is missing required field: type` + `Resource${resource.name ? ` ${resource.name}` : ""} is missing required field: type`, ); } } @@ -260,57 +298,57 @@ export function validateSpec(spec: any) { if (!param.label) { errors.push(`Param${param.param ? ` ${param.param}` : ""} is missing required field: label`); } - if (param.type && !_.includes(SpecParamType, param.type)) { + if (param.type && !Object.values(SpecParamType).includes(param.type)) { errors.push( `Invalid type ${param.type} for param${ param.param ? ` ${param.param}` : "" - }. Valid types are ${_.values(SpecParamType).join(", ")}` + }. Valid types are ${Object.values(SpecParamType).join(", ")}`, ); } - if (!param.type || param.type == SpecParamType.STRING) { + if (!param.type || param.type === SpecParamType.STRING) { // ParamType defaults to STRING if (param.options) { errors.push( `Param${ param.param ? ` ${param.param}` : "" - } cannot have options because it is type STRING` + } cannot have options because it is type STRING`, ); } } if ( param.type && - (param.type == SpecParamType.SELECT || param.type == SpecParamType.MULTISELECT) + (param.type === SpecParamType.SELECT || param.type === SpecParamType.MULTISELECT) ) { if (param.validationRegex) { errors.push( `Param${ param.param ? ` ${param.param}` : "" - } cannot have validationRegex because it is type ${param.type}` + } cannot have validationRegex because it is type ${param.type}`, ); } if (!param.options) { errors.push( `Param${param.param ? ` ${param.param}` : ""} requires options because it is type ${ param.type - }` + }`, ); } for (const opt of param.options || []) { - if (opt.value == undefined) { + if (opt.value === undefined) { errors.push( `Option for param${ param.param ? ` ${param.param}` : "" - } is missing required field: value` + } is missing required field: value`, ); } } } - if (param.type && param.type == SpecParamType.SELECTRESOURCE) { + if (param.type && param.type === SpecParamType.SELECTRESOURCE) { if (!param.resourceType) { errors.push( `Param${param.param ? ` ${param.param}` : ""} must have resourceType because it is type ${ param.type - }` + }`, ); } } @@ -340,7 +378,7 @@ export async function promptForValidInstanceId(instanceId: string): Promise { + let repoIsValid = false; + let extensionRoot = ""; + while (!repoIsValid) { + extensionRoot = await promptOnce({ + type: "input", + message: "Enter the GitHub repo URI where this extension's source code is located:", + }); + if (!repoRegex.test(extensionRoot)) { + logger.info("Repo URI must follow this format: https://github.com//"); + } else { + repoIsValid = true; + } + } + return extensionRoot; +} + +/** + * Prompts for an extension root. + * + * @param defaultRoot the default extension root + */ +export async function promptForExtensionRoot(defaultRoot: string): Promise { + return await promptOnce({ + type: "input", + message: + "Enter this extension's root directory in the repo (defaults to previous root if set):", + default: defaultRoot, + }); +} + +/** + * Prompts for the extension version's release stage. + * + * @param versionByStage map from stage to the next version to upload + * @param autoReview whether the stable version will be automatically sent for review on upload + * @param allowStable whether to allow stable versions + * @param hasVersions whether there have been any pre-release versions uploaded already + */ +async function promptForReleaseStage(args: { + versionByStage: Map; + autoReview: boolean; + allowStable: boolean; + hasVersions: boolean; + nonInteractive: boolean; + force: boolean; +}): Promise { + let stage: ReleaseStage = "rc"; + if (!args.nonInteractive) { + const choices = [ + { name: `Release candidate (${args.versionByStage.get("rc")})`, value: "rc" }, + { name: `Alpha (${args.versionByStage.get("alpha")})`, value: "alpha" }, + { name: `Beta (${args.versionByStage.get("beta")})`, value: "beta" }, + ]; + if (args.allowStable) { + const stableChoice = { + name: `Stable (${args.versionByStage.get("stable")}${ + args.autoReview ? ", automatically sent for review" : "" + })`, + value: "stable", + }; + choices.push(stableChoice); + } + stage = await promptOnce({ + type: "list", + message: "Choose the release stage:", + choices: choices, + default: stage, + }); + if (stage === "stable" && !args.hasVersions) { + logger.info( + `${clc.bold( + clc.yellow("Warning:"), + )} It's highly recommended to first upload a pre-release version before choosing stable.`, + ); + const confirmed = await confirm({ + nonInteractive: args.nonInteractive, + force: args.force, + default: false, + }); + if (!confirmed) { + stage = await promptOnce({ + type: "list", + message: "Choose the release stage:", + choices: choices, + default: stage, + }); + } + } + } + return stage; +} + export async function ensureExtensionsApiEnabled(options: any): Promise { - const projectId = needProjectId(options); - return await ensure( - projectId, - "firebaseextensions.googleapis.com", - "extensions", - options.markdown - ); + const projectId = getProjectId(options); + if (!projectId) { + return; + } + return await ensure(projectId, extensionsOrigin(), "extensions", options.markdown); +} + +export async function ensureExtensionsPublisherApiEnabled(options: any): Promise { + const projectId = getProjectId(options); + if (!projectId) { + return; + } + return await ensure(projectId, extensionsPublisherOrigin(), "extensions", options.markdown); } /** @@ -375,138 +515,486 @@ async function archiveAndUploadSource(extPath: string, bucketName: string): Prom } /** + * Gets a list of the next version to upload by release stage. * - * @param publisherId the publisher profile to publish this extension under. - * @param extensionId the ID of the extension. This must match the `name` field of extension.yaml. - * @param rootDirectory the directory containing extension.yaml + * @param extensionRef the ref of the extension + * @param version the new version of the extension */ -export async function publishExtensionVersionFromLocalSource(args: { - publisherId: string; - extensionId: string; - rootDirectory: string; - nonInteractive: boolean; - force: boolean; -}): Promise { - const extensionSpec = await getLocalExtensionSpec(args.rootDirectory); - if (extensionSpec.name != args.extensionId) { +export async function getNextVersionByStage( + extensionRef: string, + newVersion: string, +): Promise<{ versionByStage: Map; hasVersions: boolean }> { + let extensionVersions: ExtensionVersion[] = []; + try { + extensionVersions = await listExtensionVersions(extensionRef, `id="${newVersion}"`, true); + } catch (err: any) { + // Silently fail if no extension versions exist. + } + // Maps stage to default next version (e.g. "rc" => "1.0.0-rc.0"). + const versionByStage = new Map( + ["rc", "alpha", "beta"].map((stage) => [ + stage, + semver.inc(`${newVersion}-${stage}`, "prerelease", undefined, stage)!, + ]), + ); + for (const extensionVersion of extensionVersions) { + const version = semver.parse(extensionVersion.spec.version)!; + if (!version.prerelease.length) { + continue; + } + // Extensions only support a single prerelease annotation. + const prerelease = semver.prerelease(version)![0]; + // Parse out stage from prerelease (e.g. "rc" from "rc.0"). + const stage = prerelease.split(".")[0]; + if (versionByStage.has(stage) && semver.gte(version, versionByStage.get(stage)!)) { + versionByStage.set(stage, semver.inc(version, "prerelease", undefined, stage)!); + } + } + versionByStage.set("stable", newVersion); + return { versionByStage, hasVersions: extensionVersions.length > 0 }; +} + +/** + * Validates the extension spec. + * + * @param rootDirectory the directory with the extension source + * @param extensionRef the ref of the extension + */ +async function validateExtensionSpec( + rootDirectory: string, + extensionId: string, +): Promise { + const extensionSpec = await getLocalExtensionSpec(rootDirectory); + if (extensionSpec.name !== extensionId) { throw new FirebaseError( `Extension ID '${clc.bold( - args.extensionId - )}' does not match the name in extension.yaml '${clc.bold(extensionSpec.name)}'.` + extensionId, + )}' does not match the name in extension.yaml '${clc.bold(extensionSpec.name)}'.`, ); } - // Substitute deepcopied spec with autopopulated params, and make sure that it passes basic extension.yaml validation. const subbedSpec = JSON.parse(JSON.stringify(extensionSpec)); subbedSpec.params = substituteParams( extensionSpec.params || [], - AUTOPOULATED_PARAM_PLACEHOLDERS + AUTOPOULATED_PARAM_PLACEHOLDERS, ); validateSpec(subbedSpec); + return extensionSpec; +} - let extension; - try { - extension = await getExtension(`${args.publisherId}/${args.extensionId}`); - } catch (err) { - // Silently fail and continue the publish flow if extension not found. - } - +/** + * Validates the release notes. + * + * @param rootDirectory the directory with the extension source + * @param newVersion the new extension version + */ +function validateReleaseNotes(rootDirectory: string, newVersion: string, extension?: Extension) { let notes: string; try { - const changes = getLocalChangelog(args.rootDirectory); - notes = changes[extensionSpec.version]; - } catch (err) { + const changes = getLocalChangelog(rootDirectory); + notes = changes[newVersion]; + } catch (err: any) { throw new FirebaseError( "No CHANGELOG.md file found. " + "Please create one and add an entry for this version. " + marked( - "See https://firebase.google.com/docs/extensions/alpha/create-user-docs#writing-changelog for more details." - ) + "See https://firebase.google.com/docs/extensions/publishers/user-documentation#writing-changelog for more details.", + ), ); } - if (!notes && extension) { - // If this is not the first version of this extension, we require release notes + // Notes are required for all stable versions after the initial release. + if (!notes && !semver.prerelease(newVersion) && extension) { throw new FirebaseError( - `No entry for version ${extensionSpec.version} found in CHANGELOG.md. ` + + `No entry for version ${newVersion} found in CHANGELOG.md. ` + "Please add one so users know what has changed in this version. " + marked( - "See https://firebase.google.com/docs/extensions/alpha/create-user-docs#writing-changelog for more details." - ) + "See https://firebase.google.com/docs/extensions/publishers/user-documentation#writing-changelog for more details.", + ), + ); + } + return notes; +} + +/** + * Validates the extension version. + * + * @param extensionRef the ref of the extension + * @param newVersion the new extension version + * @param latestVersion the latest extension version + */ +function validateVersion(extensionRef: string, newVersion: string, latestVersion?: string) { + if (latestVersion) { + if (semver.lt(newVersion, latestVersion)) { + throw new FirebaseError( + `The version you are trying to publish (${clc.bold( + newVersion, + )}) is lower than the current version (${clc.bold( + latestVersion, + )}) for the extension '${clc.bold( + extensionRef, + )}'. Make sure this version is greater than the current version (${clc.bold( + latestVersion, + )}) inside of extension.yaml and try again.\n`, + { exit: 104 }, + ); + } else if (semver.eq(newVersion, latestVersion)) { + throw new FirebaseError( + `The version you are trying to upload (${clc.bold( + newVersion, + )}) already exists for extension '${clc.bold( + extensionRef, + )}'. Increment the version inside of extension.yaml and try again.\n`, + { exit: 103 }, + ); + } + } +} + +/** Unpacks extension state into a more specific string. */ +export function unpackExtensionState(extension: Extension) { + switch (extension.state) { + case "PUBLISHED": + // Unpacking legacy "published" terminology. + if (extension.latestApprovedVersion) { + return clc.bold(clc.green("Published")); + } else if (extension.latestVersion) { + return clc.green("Uploaded"); + } else { + return "Prerelease"; + } + case "DEPRECATED": + return clc.red("Deprecated"); + case "SUSPENDED": + return clc.bold(clc.red("Suspended")); + default: + return "-"; + } +} + +/** + * Displays metadata about the extension being uploaded. + * + * @param extensionRef the ref of the extension + */ +function displayExtensionHeader( + extensionRef: string, + extension?: Extension, + extensionRoot?: string, +) { + if (extension) { + let source = "Local source"; + if (extension.repoUri) { + const uri = new URL(extension.repoUri!); + uri.pathname = path.join(uri.pathname, extensionRoot ?? ""); + source = `${uri.toString()} (use --repo and --root to modify)`; + } + logger.info( + `\n${clc.bold("Extension:")} ${extension.ref}\n` + + `${clc.bold("State:")} ${unpackExtensionState(extension)}\n` + + `${clc.bold("Latest Version:")} ${extension.latestVersion ?? "-"}\n` + + `${clc.bold("Version in Extensions Hub:")} ${extension.latestApprovedVersion ?? "-"}\n` + + `${clc.bold("Source in GitHub:")} ${source}\n`, + ); + } else { + logger.info( + `\n${clc.bold("Extension:")} ${extensionRef}\n` + + `${clc.bold("State:")} ${clc.bold(clc.blue("New"))}\n`, + ); + } +} + +/** + * Fetches the extension source from GitHub. + * + * @param repoUri the public GitHub repo URI that contains the extension source + * @param sourceRef the commit hash, branch, or tag to build from the repo + * @param extensionRoot the root directory that contains this extension + */ +async function fetchExtensionSource( + repoUri: string, + sourceRef: string, + extensionRoot: string, +): Promise { + const sourceUri = repoUri + path.join("/tree", sourceRef, extensionRoot); + logger.info(`Validating source code at ${clc.bold(sourceUri)}...`); + const archiveUri = `${repoUri}/archive/${sourceRef}.zip`; + const tempDirectory = tmp.dirSync({ unsafeCleanup: true }); + const archiveErrorMessage = `Failed to extract archive from ${clc.bold( + archiveUri, + )}. Please check that the repo is public and that the source ref is valid.`; + try { + const response = await fetch(archiveUri); + if (response.ok) { + await response.body.pipe(createUnzipTransform(tempDirectory.name)).promise(); + } + } catch (err: any) { + throw new FirebaseError(archiveErrorMessage); + } + const archiveName = fs.readdirSync(tempDirectory.name)[0]; + if (!archiveName) { + throw new FirebaseError(archiveErrorMessage); + } + const rootDirectory = path.join(tempDirectory.name, archiveName, extensionRoot); + // Pre-validation to show a more useful error message in the context of a temp directory. + try { + readFile(path.resolve(rootDirectory, EXTENSIONS_SPEC_FILE)); + } catch (err: any) { + throw new FirebaseError( + `Failed to find ${clc.bold(EXTENSIONS_SPEC_FILE)} in directory ${clc.bold( + extensionRoot, + )}. Please verify the root and try again.`, ); } - displayReleaseNotes(args.publisherId, args.extensionId, extensionSpec.version, notes); - if ( - !(await confirm({ + return rootDirectory; +} + +/** + * Uploads an extension version from a GitHub repo. + * + * @param publisherId the ID of the Publisher this Extension will be published under + * @param extensionId the ID of the Extension to be published + * @param repoUri the URI of the repo where this Extension's source exists + * @param sourceRef the commit hash, branch, or tag name in the repo to publish from + * @param extensionRoot the root directory that contains this Extension's source + * @param stage the release stage to publish + * @param nonInteractive whether to display prompts + * @param force whether to force confirmations + */ +export async function uploadExtensionVersionFromGitHubSource(args: { + publisherId: string; + extensionId: string; + repoUri?: string; + sourceRef?: string; + extensionRoot?: string; + stage?: ReleaseStage; + nonInteractive: boolean; + force: boolean; +}): Promise { + const extensionRef = `${args.publisherId}/${args.extensionId}`; + let extension: Extension | undefined; + let latestVersion: ExtensionVersion | undefined; + try { + extension = await getExtension(extensionRef); + latestVersion = await getExtensionVersion(`${extensionRef}@latest`); + } catch (err: any) { + // Silently fail and continue if extension is new or has no latest version set. + } + displayExtensionHeader(extensionRef, extension, latestVersion?.extensionRoot); + + if (args.stage && !stageOptions.includes(args.stage)) { + throw new FirebaseError( + `--stage only supports the following values: ${stageOptions.join(", ")}`, + ); + } + + // Prompt for repo URI. + if (args.repoUri && !repoRegex.test(args.repoUri)) { + throw new FirebaseError("Repo URI must follow this format: https://github.com//"); + } + let repoUri = args.repoUri || extension?.repoUri; + if (!repoUri) { + if (!args.nonInteractive) { + repoUri = await promptForValidRepoURI(); + } else { + throw new FirebaseError("Repo URI is required but not currently set."); + } + } + + let extensionRoot = args.extensionRoot || latestVersion?.extensionRoot; + if (!extensionRoot) { + const defaultRoot = "/"; + if (!args.nonInteractive) { + extensionRoot = await promptForExtensionRoot(defaultRoot); + } else { + extensionRoot = defaultRoot; + } + } + // Normalize root path and strip leading and trailing slashes and all `../`. + const normalizedRoot = path + .normalize(extensionRoot) + .replaceAll(/^\/|\/$/g, "") + .replaceAll(/^(\.\.\/)*/g, ""); + extensionRoot = normalizedRoot || "/"; + + // Prompt for source ref and default to HEAD. + let sourceRef = args.sourceRef; + const defaultSourceRef = "HEAD"; + if (!sourceRef) { + if (!args.nonInteractive) { + sourceRef = await promptOnce({ + type: "input", + message: "Enter the commit hash, branch, or tag name to build from in the repo:", + default: defaultSourceRef, + }); + } else { + sourceRef = defaultSourceRef; + } + } + + const rootDirectory = await fetchExtensionSource(repoUri, sourceRef, extensionRoot); + const extensionSpec = await validateExtensionSpec(rootDirectory, args.extensionId); + validateVersion(extensionRef, extensionSpec.version, extension?.latestVersion); + const { versionByStage, hasVersions } = await getNextVersionByStage( + extensionRef, + extensionSpec.version, + ); + const autoReview = + !!extension?.latestApprovedVersion || + latestVersion?.listing?.state === "PENDING" || + latestVersion?.listing?.state === "APPROVED" || + latestVersion?.listing?.state === "REJECTED"; + + // Prompt for release stage. + let stage = args.stage; + if (!stage) { + stage = await promptForReleaseStage({ + versionByStage, + autoReview, + allowStable: true, + hasVersions, nonInteractive: args.nonInteractive, force: args.force, - default: false, - })) - ) { + }); + } + + const newVersion = versionByStage.get(stage)!; + const releaseNotes = validateReleaseNotes(rootDirectory, extensionSpec.version, extension); + const sourceUri = repoUri + path.join("/tree", sourceRef, extensionRoot); + displayReleaseNotes({ + extensionRef, + newVersion, + releaseNotes, + sourceUri, + autoReview: stage === "stable" && autoReview, + }); + const confirmed = await confirm({ + nonInteractive: args.nonInteractive, + force: args.force, + default: false, + }); + if (!confirmed) { return; } - if ( - extension && - extension.latestVersion && - semver.lt(extensionSpec.version, extension.latestVersion) - ) { - // publisher's version is less than current latest version. - throw new FirebaseError( - `The version you are trying to publish (${clc.bold( - extensionSpec.version - )}) is lower than the current version (${clc.bold( - extension.latestVersion - )}) for the extension '${clc.bold( - `${args.publisherId}/${args.extensionId}` - )}'. Please make sure this version is greater than the current version (${clc.bold( - extension.latestVersion - )}) inside of extension.yaml.\n` - ); - } else if ( - extension && - extension.latestVersion && - semver.eq(extensionSpec.version, extension.latestVersion) - ) { - // publisher's version is equal to the current latest version. + // Upload the extension version. + const extensionVersionRef = `${extensionRef}@${newVersion}`; + const uploadSpinner = ora(`Uploading ${clc.bold(extensionVersionRef)}...`); + let res; + try { + uploadSpinner.start(); + res = await createExtensionVersionFromGitHubSource({ + extensionVersionRef, + extensionRoot, + repoUri, + sourceRef: sourceRef, + }); + uploadSpinner.succeed(`Successfully uploaded ${clc.bold(extensionRef)}`); + } catch (err: any) { + uploadSpinner.fail(); + if (err.status === 404) { + throw getMissingPublisherError(args.publisherId); + } + throw err; + } + return res; +} + +/** + * Uploads an extension version from local source. + * + * @param publisherId the ID of the Publisher this Extension will be published under + * @param extensionId the ID of the Extension to be published + * @param rootDirectory the root directory that contains this Extension's source + * @param stage the release stage to publish + * @param nonInteractive whether to display prompts + * @param force whether to force confirmations + */ +export async function uploadExtensionVersionFromLocalSource(args: { + publisherId: string; + extensionId: string; + rootDirectory: string; + stage: ReleaseStage; + nonInteractive: boolean; + force: boolean; +}): Promise { + const extensionRef = `${args.publisherId}/${args.extensionId}`; + let extension: Extension | undefined; + let latestVersion: ExtensionVersion | undefined; + try { + extension = await getExtension(extensionRef); + latestVersion = await getExtensionVersion(`${extensionRef}@latest`); + } catch (err: any) { + // Silently fail and continue if extension is new or has no latest version set. + } + displayExtensionHeader(extensionRef, extension, latestVersion?.extensionRoot); + + const localStageOptions = ["rc", "alpha", "beta"]; + if (args.stage && !localStageOptions.includes(args.stage)) { throw new FirebaseError( - `The version you are trying to publish (${clc.bold( - extensionSpec.version - )}) already exists for the extension '${clc.bold( - `${args.publisherId}/${args.extensionId}` - )}'. Please increment the version inside of extension.yaml.\n`, - { exit: 103 } + `--stage only supports the following values when used with --local: ${localStageOptions.join( + ", ", + )}`, ); } - const ref = `${args.publisherId}/${args.extensionId}@${extensionSpec.version}`; + const extensionSpec = await validateExtensionSpec(args.rootDirectory, args.extensionId); + validateVersion(extensionRef, extensionSpec.version, extension?.latestVersion); + const { versionByStage } = await getNextVersionByStage(extensionRef, extensionSpec.version); + + // Prompt for release stage. + let stage = args.stage; + if (!stage) { + if (!args.nonInteractive) { + stage = await promptForReleaseStage({ + versionByStage, + autoReview: false, + allowStable: false, + hasVersions: false, + nonInteractive: args.nonInteractive, + force: args.force, + }); + } else { + stage = "rc"; + } + } + + const newVersion = versionByStage.get(stage)!; + const releaseNotes = validateReleaseNotes(args.rootDirectory, extensionSpec.version, extension); + displayReleaseNotes({ extensionRef, newVersion, releaseNotes, autoReview: false }); + const confirmed = await confirm({ + nonInteractive: args.nonInteractive, + force: args.force, + default: false, + }); + if (!confirmed) { + return; + } + + const extensionVersionRef = `${extensionRef}@${newVersion}`; let packageUri: string; let objectPath = ""; - const uploadSpinner = ora.default(" Archiving and uploading extension source code"); + const uploadSpinner = ora("Archiving and uploading extension source code..."); try { uploadSpinner.start(); objectPath = await archiveAndUploadSource(args.rootDirectory, EXTENSIONS_BUCKET_NAME); - uploadSpinner.succeed(" Uploaded extension source code"); - packageUri = storageOrigin + objectPath + "?alt=media"; - } catch (err) { + uploadSpinner.succeed("Uploaded extension source code"); + packageUri = storageOrigin() + objectPath + "?alt=media"; + } catch (err: any) { uploadSpinner.fail(); - throw err; + throw new FirebaseError(`Failed to archive and upload extension source code, ${err}`, { + original: err, + }); } - const publishSpinner = ora.default(`Publishing ${clc.bold(ref)}`); + const publishSpinner = ora(`Uploading ${clc.bold(extensionVersionRef)}...`); let res; try { publishSpinner.start(); - res = await publishExtensionVersion(ref, packageUri); - publishSpinner.succeed(` Successfully published ${clc.bold(ref)}`); - } catch (err) { + res = await createExtensionVersionFromLocalSource({ extensionVersionRef, packageUri }); + publishSpinner.succeed(`Successfully uploaded ${clc.bold(extensionVersionRef)}`); + } catch (err: any) { publishSpinner.fail(); - if (err.status == 404) { - throw new FirebaseError( - marked( - `Couldn't find publisher ID '${clc.bold( - args.publisherId - )}'. Please ensure that you have registered this ID. To register as a publisher, you can check out the [Firebase documentation](https://firebase.google.com/docs/extensions/alpha/share#register_as_an_extensions_publisher) for step-by-step instructions.` - ) - ); + if (err.status === 404) { + throw getMissingPublisherError(args.publisherId); } throw err; } @@ -514,6 +1002,16 @@ export async function publishExtensionVersionFromLocalSource(args: { return res; } +export function getMissingPublisherError(publisherId: string): FirebaseError { + return new FirebaseError( + marked( + `Couldn't find publisher ID '${clc.bold( + publisherId, + )}'. Please ensure that you have registered this ID. For step-by-step instructions on getting started as a publisher, see https://firebase.google.com/docs/extensions/publishers/get-started.`, + ), + ); +} + /** * Creates a source from a local path or URL. If a local path is given, it will be zipped * and uploaded to EXTENSIONS_BUCKET_NAME, and then deleted after the source is created. @@ -522,31 +1020,34 @@ export async function publishExtensionVersionFromLocalSource(args: { */ export async function createSourceFromLocation( projectId: string, - sourceUri: string + sourceUri: string, ): Promise { + const extensionRoot = "/"; let packageUri: string; - let extensionRoot: string; let objectPath = ""; - if (!URL_REGEX.test(sourceUri)) { - const uploadSpinner = ora.default(" Archiving and uploading extension source code"); - try { - uploadSpinner.start(); - objectPath = await archiveAndUploadSource(sourceUri, EXTENSIONS_BUCKET_NAME); - uploadSpinner.succeed(" Uploaded extension source code"); - packageUri = storageOrigin + objectPath + "?alt=media"; - extensionRoot = "/"; - } catch (err) { - uploadSpinner.fail(); - throw err; - } - } else { - [packageUri, extensionRoot] = sourceUri.split("#"); + + const spinner = ora(" Archiving and uploading extension source code"); + try { + spinner.start(); + objectPath = await archiveAndUploadSource(sourceUri, EXTENSIONS_BUCKET_NAME); + spinner.succeed(" Uploaded extension source code"); + + packageUri = storageOrigin() + objectPath + "?alt=media"; + const res = await createSource(projectId, packageUri, extensionRoot); + logger.debug("Created new Extension Source %s", res.name); + + // if we uploaded an object to user's bucket, delete it after "createSource" copies it into extension service's bucket. + await deleteUploadedSource(objectPath); + return res; + } catch (err: any) { + spinner.fail(); + throw new FirebaseError( + `Failed to archive and upload extension source from ${sourceUri}, ${err}`, + { + original: err, + }, + ); } - const res = await createSource(projectId, packageUri, extensionRoot); - logger.debug("Created new Extension Source %s", res.name); - // if we uploaded an object, delete it - await deleteUploadedSource(objectPath); - return res; } /** @@ -557,55 +1058,56 @@ async function deleteUploadedSource(objectPath: string) { try { await deleteObject(objectPath); logger.debug("Cleaned up uploaded source archive"); - } catch (err) { + } catch (err: any) { logger.debug("Unable to clean up uploaded source archive"); } } } /** - * Looks up a ExtensionSource from a extensionName. If no source exists for that extensionName, returns undefined. - * @param extensionName a official extension source name - * or a One-Platform format source name (/project//sources/) - * @return an ExtensionSource corresponding to extensionName if one exists, undefined otherwise + * Parses the publisher project number from publisher profile name. */ -export async function getExtensionSourceFromName(extensionName: string): Promise { - const officialExtensionRegex = /^[a-zA-Z\-]+[0-9@.]*$/; - const existingSourceRegex = /projects\/.+\/sources\/.+/; - // if the provided extensionName contains only letters and hyphens, assume it is an official extension - if (officialExtensionRegex.test(extensionName)) { - const [name, version] = extensionName.split("@"); - const registryEntry = await resolveRegistryEntry(name); - const sourceUrl = resolveSourceUrl(registryEntry, name, version); - return await getSource(sourceUrl); - } else if (existingSourceRegex.test(extensionName)) { - logger.info(`Fetching the source "${extensionName}"...`); - return await getSource(extensionName); - } - throw new FirebaseError(`Could not find an extension named '${extensionName}'. `); +export function getPublisherProjectFromName(publisherName: string): number { + const publisherNameRegex = /projects\/.+\/publisherProfile/; + + if (publisherNameRegex.test(publisherName)) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, projectNumber, __] = publisherName.split("/"); + return Number.parseInt(projectNumber); + } + throw new FirebaseError(`Could not find publisher with name '${publisherName}'.`); } /** - * Confirm the version number in extension.yaml with the user . + * Displays the release notes and confirmation message for the extension to be uploaded. * - * @param publisherId the publisher ID of the extension being installed - * @param extensionId the extension ID of the extension being installed - * @param versionId the version ID of the extension being installed + * @param extensionRef the ref of the extension + * @param newVersion the new version of the extension + * @param releaseNotes the release notes for the version being uploaded (if any) + * @param sourceUri the source URI from which the extension will be uploaded */ -export function displayReleaseNotes( - publisherId: string, - extensionId: string, - versionId: string, - releaseNotes?: string -): void { - const releaseNotesMessage = releaseNotes - ? ` Release notes for this version:\n${marked(releaseNotes)}\n` +export function displayReleaseNotes(args: { + extensionRef: string; + newVersion: string; + autoReview: boolean; + releaseNotes?: string; + sourceUri?: string; +}): void { + const source = args.sourceUri || "Local source"; + const releaseNotesMessage = args.releaseNotes + ? `${clc.bold("Release notes:")}\n${marked(args.releaseNotes)}` : "\n"; + const metadataMessage = + `${clc.bold("Extension:")} ${args.extensionRef}\n` + + `${clc.bold("Version:")} ${clc.bold(clc.green(args.newVersion))} ${ + args.autoReview ? "(automatically sent for review)" : "" + }\n` + + `${clc.bold("Source:")} ${source}\n`; const message = - `You are about to publish version ${clc.green(versionId)} of ${clc.green( - `${publisherId}/${extensionId}` - )} to Firebase's registry of extensions.${releaseNotesMessage}` + - "Once an extension version is published, it cannot be changed. If you wish to make changes after publishing, you will need to publish a new version.\n\n"; + `\nYou are about to upload a new version to Firebase's registry of extensions.\n\n` + + metadataMessage + + releaseNotesMessage + + `Once an extension version is uploaded, it becomes installable by other users and cannot be changed. If you wish to make changes after uploading, you will need to upload a new version.\n`; logger.info(message); } @@ -621,7 +1123,7 @@ export async function promptForOfficialExtension(message: string): Promise { const message = `An extension with the ID '${clc.bold( - extensionName + extensionName, )}' already exists in the project '${clc.bold(projectName)}'. What would you like to do?`; const choices = [ { name: "Update or reconfigure the existing instance", value: "updateExisting" }, @@ -655,25 +1157,26 @@ export async function promptForRepeatInstance( * @param instanceId ID of the extension instance */ export async function instanceIdExists(projectId: string, instanceId: string): Promise { - const instanceRes = await getInstance(projectId, instanceId, { - resolveOnHTTPError: true, - }); - if (instanceRes.error) { - if (_.get(instanceRes, "error.code") === 404) { - return false; - } - const msg = - "Unexpected error when checking if instance ID exists: " + - _.get(instanceRes, "error.message"); - throw new FirebaseError(msg, { - original: instanceRes.error, - }); + try { + await getInstance(projectId, instanceId); + } catch (err: unknown) { + if (err instanceof FirebaseError) { + if (err.status === 404) { + return false; + } + const msg = `Unexpected error when checking if instance ID exists: ${err}`; + throw new FirebaseError(msg, { + original: err, + }); + } else { + throw err; + } } return true; } export function isUrlPath(extInstallPath: string): boolean { - return URL_REGEX.test(extInstallPath); + return extInstallPath.startsWith("https:"); } export function isLocalPath(extInstallPath: string): boolean { @@ -708,7 +1211,7 @@ export function getSourceOrigin(sourceOrVersion: string): SourceOrigin { let ref; try { ref = refs.parse(sourceOrVersion); - } catch (err) { + } catch (err: any) { // Silently fail. } if (ref && ref.publisherId && ref.extensionId && !ref.version) { @@ -719,29 +1222,18 @@ export function getSourceOrigin(sourceOrVersion: string): SourceOrigin { } throw new FirebaseError( `Could not find source '${clc.bold( - sourceOrVersion - )}'. Check to make sure the source is correct, and then please try again.` + sourceOrVersion, + )}'. Check to make sure the source is correct, and then please try again.`, ); } -/** - * Confirm if the user wants to continue - */ -export async function confirm(args: { - nonInteractive?: boolean; - force?: boolean; - default?: boolean; -}): Promise { - if (!args.nonInteractive && !args.force) { - const message = `Do you wish to continue?`; - return await promptOnce({ - type: "confirm", - message, - default: args.default, - }); - } else if (args.nonInteractive && !args.force) { - throw new FirebaseError("Pass the --force flag to use this command in non-interactive mode"); - } else { - return true; +export async function diagnoseAndFixProject(options: any): Promise { + const projectId = getProjectId(options); + if (!projectId) { + return; + } + const ok = await diagnose(projectId); + if (!ok) { + throw new FirebaseError("Unable to proceed until all issues are resolved."); } } diff --git a/src/test/extensions/listExtensions.spec.ts b/src/extensions/listExtensions.spec.ts similarity index 85% rename from src/test/extensions/listExtensions.spec.ts rename to src/extensions/listExtensions.spec.ts index bbfadf4f4fb..0d50ad60c15 100644 --- a/src/test/extensions/listExtensions.spec.ts +++ b/src/extensions/listExtensions.spec.ts @@ -1,8 +1,8 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as extensionsApi from "../../extensions/extensionsApi"; -import { listExtensions } from "../../extensions/listExtensions"; +import * as extensionsApi from "./extensionsApi"; +import { listExtensions } from "./listExtensions"; const MOCK_INSTANCES = [ { @@ -12,8 +12,7 @@ const MOCK_INSTANCES = [ state: "ACTIVE", config: { extensionRef: "firebase/image-resizer", - name: - "projects/my-test-proj/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + name: "projects/my-test-proj/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", createTime: "2019-05-19T00:20:10.416947Z", source: { state: "ACTIVE", @@ -34,8 +33,7 @@ const MOCK_INSTANCES = [ state: "ACTIVE", config: { extensionRef: "firebase/image-resizer", - name: - "projects/my-test-proj/instances/image-resizer-1/configurations/5b1fb749-764d-4bd1-af60-bb7f22d27860", + name: "projects/my-test-proj/instances/image-resizer-1/configurations/5b1fb749-764d-4bd1-af60-bb7f22d27860", createTime: "2019-06-19T00:21:06.722782Z", source: { spec: { diff --git a/src/extensions/listExtensions.ts b/src/extensions/listExtensions.ts index 2e6d3d29c97..c6cb663f1a3 100644 --- a/src/extensions/listExtensions.ts +++ b/src/extensions/listExtensions.ts @@ -1,24 +1,23 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; -import Table = require("cli-table"); +import * as clc from "colorette"; +const Table = require("cli-table"); -import { ExtensionInstance, listInstances } from "./extensionsApi"; +import { listInstances } from "./extensionsApi"; +import { logger } from "../logger"; +import { last, logLabeledBullet } from "../utils"; import { logPrefix } from "./extensionsHelper"; -import * as utils from "../utils"; import * as extensionsUtils from "./utils"; -import { logger } from "../logger"; /** * Lists the extensions installed under a project * @param projectId ID of the project we're querying * @return mapping that contains a list of instances under the "instances" key */ -export async function listExtensions(projectId: string): Promise { +export async function listExtensions(projectId: string): Promise[]> { const instances = await listInstances(projectId); if (instances.length < 1) { - utils.logLabeledBullet( + logLabeledBullet( logPrefix, - `there are no extensions installed on project ${clc.bold(projectId)}.` + `there are no extensions installed on project ${clc.bold(projectId)}.`, ); return []; } @@ -28,21 +27,23 @@ export async function listExtensions(projectId: string): Promise { style: { head: ["yellow"] }, }); // Order instances newest to oldest. - const sorted = _.sortBy(instances, "createTime", "asc").reverse(); + const sorted = instances.sort( + (a, b) => new Date(b.createTime).valueOf() - new Date(a.createTime).valueOf(), + ); const formatted: Record[] = []; sorted.forEach((instance) => { - let extension = _.get(instance, "config.extensionRef", ""); + let extension = instance.config.extensionRef || ""; let publisher; if (extension === "") { - extension = _.get(instance, "config.source.spec.name", ""); + extension = instance.config.source.spec.name || ""; publisher = "N/A"; } else { publisher = extension.split("/")[0]; } - const instanceId = _.last(instance.name.split("/")) ?? ""; + const instanceId = last(instance.name.split("/")) ?? ""; const state = instance.state + - (_.get(instance, "config.source.state", "ACTIVE") === "DELETED" ? " (UNPUBLISHED)" : ""); + ((instance.config.source.state || "ACTIVE") === "DELETED" ? " (UNPUBLISHED)" : ""); const version = instance?.config?.source?.spec?.version; const updateTime = extensionsUtils.formatTimestamp(instance.updateTime); table.push([extension, publisher, instanceId, state, version, updateTime]); @@ -56,6 +57,7 @@ export async function listExtensions(projectId: string): Promise { }); }); - utils.logLabeledBullet(logPrefix, `list of extensions installed in ${clc.bold(projectId)}:`); + logLabeledBullet(logPrefix, `list of extensions installed in ${clc.bold(projectId)}:`); + logger.info(table.toString()); return formatted; } diff --git a/src/extensions/localHelper.spec.ts b/src/extensions/localHelper.spec.ts new file mode 100644 index 00000000000..8b4be8b2127 --- /dev/null +++ b/src/extensions/localHelper.spec.ts @@ -0,0 +1,87 @@ +import { expect } from "chai"; +import * as fs from "fs-extra"; +import * as yaml from "yaml"; +import * as sinon from "sinon"; + +import * as localHelper from "./localHelper"; +import { FirebaseError } from "../error"; +import { FIXTURE_DIR as EXT_FIXTURE_DIRECTORY } from "../test/fixtures/extension-yamls/sample-ext"; +import { FIXTURE_DIR as EXT_PREINSTALL_FIXTURE_DIRECTORY } from "../test/fixtures/extension-yamls/sample-ext-preinstall"; +import { FIXTURE_DIR as INVALID_EXT_DIRECTORY } from "../test/fixtures/extension-yamls/invalid"; + +describe("localHelper", () => { + const sandbox = sinon.createSandbox(); + + describe("getLocalExtensionSpec", () => { + it("should return a spec when extension.yaml is present", async () => { + const result = await localHelper.getLocalExtensionSpec(EXT_FIXTURE_DIRECTORY); + expect(result.name).to.equal("fixture-ext"); + expect(result.version).to.equal("1.0.0"); + expect(result.preinstallContent).to.be.undefined; + }); + + it("should populate preinstallContent when PREINSTALL.md is present", async () => { + const result = await localHelper.getLocalExtensionSpec(EXT_PREINSTALL_FIXTURE_DIRECTORY); + expect(result.name).to.equal("fixture-ext-with-preinstall"); + expect(result.version).to.equal("1.0.0"); + expect(result.preinstallContent).to.equal("This is a PREINSTALL file for testing with.\n"); + }); + + it("should return a nice error if there is no extension.yaml", async () => { + await expect(localHelper.getLocalExtensionSpec(__dirname)).to.be.rejectedWith(FirebaseError); + }); + + describe("with an invalid YAML file", () => { + it("should return a rejected promise with a useful error if extension.yaml is invalid", async () => { + await expect(localHelper.getLocalExtensionSpec(INVALID_EXT_DIRECTORY)).to.be.rejectedWith( + FirebaseError, + /YAML Error.+Implicit keys need to be on a single line.+line 2.+/, + ); + }); + }); + + describe("other YAML errors", () => { + beforeEach(() => { + sandbox.stub(yaml, "parse").throws(new Error("not the files you are looking for")); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should rethrow normal errors", async () => { + await expect(localHelper.getLocalExtensionSpec(EXT_FIXTURE_DIRECTORY)).to.be.rejectedWith( + FirebaseError, + "not the files you are looking for", + ); + }); + }); + }); + + describe("isLocalExtension", () => { + let fsStub: sinon.SinonStub; + beforeEach(() => { + fsStub = sandbox.stub(fs, "readdirSync"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return true if a file exists there", () => { + fsStub.returns(""); + + const result = localHelper.isLocalExtension("some/local/path"); + + expect(result).to.be.true; + }); + + it("should return false if a file doesn't exist there", () => { + fsStub.throws(new Error("directory not found")); + + const result = localHelper.isLocalExtension("some/local/path"); + + expect(result).to.be.false; + }); + }); +}); diff --git a/src/extensions/localHelper.ts b/src/extensions/localHelper.ts index 48a0f93bdef..44dcca740f3 100644 --- a/src/extensions/localHelper.ts +++ b/src/extensions/localHelper.ts @@ -1,13 +1,13 @@ import * as fs from "fs-extra"; import * as path from "path"; -import * as yaml from "js-yaml"; +import * as yaml from "yaml"; import { fileExistsSync } from "../fsutils"; import { FirebaseError } from "../error"; -import { ExtensionSpec } from "./extensionsApi"; +import { ExtensionSpec } from "./types"; import { logger } from "../logger"; -const EXTENSIONS_SPEC_FILE = "extension.yaml"; +export const EXTENSIONS_SPEC_FILE = "extension.yaml"; const EXTENSIONS_PREINSTALL_FILE = "PREINSTALL.md"; /** @@ -19,7 +19,7 @@ export async function getLocalExtensionSpec(directory: string): Promise { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + describe(`${manifest.instanceExists.name}`, () => { + it("should return true for an existing instance", () => { + const result = manifest.instanceExists("delete-user-data", generateBaseConfig()); + + expect(result).to.be.true; + }); + + it("should return false for a non-existing instance", () => { + const result = manifest.instanceExists("does-not-exist", generateBaseConfig()); + + expect(result).to.be.false; + }); + }); + + describe(`${manifest.getInstanceTarget.name}`, () => { + it("should return the correct source for a local instance", () => { + const result = manifest.getInstanceTarget( + "delete-user-data-local", + generateConfigWithLocal(), + ); + + expect(result).to.equal("./delete-user-data"); + }); + + it("should return the correct source for an instance with ref", () => { + const result = manifest.getInstanceTarget("delete-user-data", generateConfigWithLocal()); + + expect(result).to.equal("firebase/delete-user-data@0.1.12"); + }); + + it("should throw when looking for a non-existing instance", () => { + expect(() => + manifest.getInstanceTarget("does-not-exist", generateConfigWithLocal()), + ).to.throw(FirebaseError); + }); + }); + + describe(`${manifest.getInstanceRef.name}`, () => { + it("should return the correct ref for an existing instance", () => { + const result = manifest.getInstanceRef("delete-user-data", generateConfigWithLocal()); + + expect(refs.toExtensionVersionRef(result)).to.equal( + refs.toExtensionVersionRef({ + publisherId: "firebase", + extensionId: "delete-user-data", + version: "0.1.12", + }), + ); + }); + + it("should throw when looking for a non-existing instance", () => { + expect(() => manifest.getInstanceRef("does-not-exist", generateConfigWithLocal())).to.throw( + FirebaseError, + ); + }); + + it("should throw when looking for a instance with local source", () => { + expect(() => + manifest.getInstanceRef("delete-user-data-local", generateConfigWithLocal()), + ).to.throw(FirebaseError); + }); + }); + + describe(`${manifest.removeFromManifest.name}`, () => { + let deleteProjectFileStub: sinon.SinonStub; + let writeProjectFileStub: sinon.SinonStub; + let projectFileExistsStub: sinon.SinonStub; + beforeEach(() => { + deleteProjectFileStub = sandbox.stub(Config.prototype, "deleteProjectFile"); + writeProjectFileStub = sandbox.stub(Config.prototype, "writeProjectFile"); + projectFileExistsStub = sandbox.stub(Config.prototype, "projectFileExists"); + projectFileExistsStub.returns(true); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should remove from firebase.json and remove .env file", () => { + manifest.removeFromManifest("delete-user-data", generateBaseConfig()); + + expect(writeProjectFileStub).calledWithExactly("firebase.json", { + extensions: { + "delete-user-data": undefined, + "delete-user-data-gm2h": "firebase/delete-user-data@0.1.12", + }, + }); + + expect(deleteProjectFileStub).calledWithExactly("extensions/delete-user-data.env"); + }); + }); + + describe(`${manifest.writeToManifest.name}`, () => { + let askWriteProjectFileStub: sinon.SinonStub; + let writeProjectFileStub: sinon.SinonStub; + beforeEach(() => { + askWriteProjectFileStub = sandbox.stub(Config.prototype, "askWriteProjectFile"); + writeProjectFileStub = sandbox.stub(Config.prototype, "writeProjectFile"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should write to both firebase.json and env files", async () => { + await manifest.writeToManifest( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { a: { baseValue: "pikachu" }, b: { baseValue: "bulbasaur" } }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + { + instanceId: "instance-2", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "2.0.0", + }, + params: { a: { baseValue: "eevee" }, b: { baseValue: "squirtle" } }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.SECRET, + }, + { + param: "b", + label: "", + type: ParamType.SECRET, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + { nonInteractive: false, force: false }, + ); + expect(writeProjectFileStub).calledWithExactly("firebase.json", { + extensions: { + "delete-user-data": "firebase/delete-user-data@0.1.12", + "delete-user-data-gm2h": "firebase/delete-user-data@0.1.12", + "instance-1": "firebase/bigquery-export@1.0.0", + "instance-2": "firebase/bigquery-export@2.0.0", + }, + }); + + expect(askWriteProjectFileStub).to.have.been.calledTwice; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.env", + `a=pikachu\nb=bulbasaur`, + false, + ); + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-2.env", + `a=eevee\nb=squirtle`, + false, + ); + }); + + it("should write to env files in stable, alphabetical by key order", async () => { + await manifest.writeToManifest( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { b: { baseValue: "bulbasaur" }, a: { baseValue: "absol" } }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + { + instanceId: "instance-2", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "2.0.0", + }, + params: { e: { baseValue: "eevee" }, s: { baseValue: "squirtle" } }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + { nonInteractive: false, force: false }, + ); + expect(writeProjectFileStub).calledWithExactly("firebase.json", { + extensions: { + "delete-user-data": "firebase/delete-user-data@0.1.12", + "delete-user-data-gm2h": "firebase/delete-user-data@0.1.12", + "instance-1": "firebase/bigquery-export@1.0.0", + "instance-2": "firebase/bigquery-export@2.0.0", + }, + }); + + expect(askWriteProjectFileStub).to.have.been.calledTwice; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.env", + `a=absol\nb=bulbasaur`, + false, + ); + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-2.env", + `e=eevee\ns=squirtle`, + false, + ); + }); + + it("should write events-related env vars", async () => { + await manifest.writeToManifest( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { + b: { baseValue: "bulbasaur" }, + a: { baseValue: "absol" }, + EVENTARC_CHANNEL: { + baseValue: "projects/test-project/locations/us-central1/channels/firebase", + }, + ALLOWED_EVENT_TYPES: { baseValue: "google.firebase.custom-event-occurred" }, + }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + events: [ + { + type: "google.firebase.custom-event-occurred", + description: "Custom event occurred", + }, + ], + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + { + instanceId: "instance-2", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "2.0.0", + }, + params: { + e: { baseValue: "eevee" }, + s: { baseValue: "squirtle" }, + EVENTARC_CHANNEL: { + baseValue: "projects/test-project/locations/us-central1/channels/firebase", + }, + ALLOWED_EVENT_TYPES: { baseValue: "google.firebase.custom-event-occurred" }, + }, + extensionSpec: { + name: "bigquery-export", + version: "2.0.0", + resources: [], + sourceUrl: "", + events: [ + { + type: "google.firebase.custom-event-occurred", + description: "Custom event occurred", + }, + ], + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + { nonInteractive: false, force: false }, + ); + expect(writeProjectFileStub).calledWithExactly("firebase.json", { + extensions: { + "delete-user-data": "firebase/delete-user-data@0.1.12", + "delete-user-data-gm2h": "firebase/delete-user-data@0.1.12", + "instance-1": "firebase/bigquery-export@1.0.0", + "instance-2": "firebase/bigquery-export@2.0.0", + }, + }); + + expect(askWriteProjectFileStub).to.have.been.calledTwice; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.env", + "a=absol\n" + + "ALLOWED_EVENT_TYPES=google.firebase.custom-event-occurred\n" + + "b=bulbasaur\n" + + "EVENTARC_CHANNEL=projects/test-project/locations/us-central1/channels/firebase", + false, + ); + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-2.env", + "ALLOWED_EVENT_TYPES=google.firebase.custom-event-occurred\n" + + "e=eevee\n" + + "EVENTARC_CHANNEL=projects/test-project/locations/us-central1/channels/firebase\n" + + "s=squirtle", + false, + ); + }); + + it("should overwrite when user chooses to", async () => { + // Chooses to overwrite instead of merge. + sandbox.stub(prompt, "promptOnce").resolves(true); + + await manifest.writeToManifest( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { a: { baseValue: "pikachu" }, b: { baseValue: "bulbasaur" } }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + { + instanceId: "instance-2", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "2.0.0", + }, + params: { a: { baseValue: "eevee" }, b: { baseValue: "squirtle" } }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + { nonInteractive: false, force: false }, + true /** allowOverwrite */, + ); + expect(writeProjectFileStub).calledWithExactly("firebase.json", { + extensions: { + // Original list deleted here. + "instance-1": "firebase/bigquery-export@1.0.0", + "instance-2": "firebase/bigquery-export@2.0.0", + }, + }); + + expect(askWriteProjectFileStub).to.have.been.calledTwice; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.env", + `a=pikachu\nb=bulbasaur`, + false, + ); + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-2.env", + `a=eevee\nb=squirtle`, + false, + ); + }); + + it("should not write empty values", async () => { + // Chooses to overwrite instead of merge. + sandbox.stub(prompt, "promptOnce").resolves(true); + + await manifest.writeToManifest( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { a: { baseValue: "pikachu" }, b: { baseValue: "" } }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.STRING, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + { nonInteractive: false, force: false }, + true /** allowOverwrite */, + ); + expect(writeProjectFileStub).calledWithExactly("firebase.json", { + extensions: { + // Original list deleted here. + "instance-1": "firebase/bigquery-export@1.0.0", + }, + }); + + expect(askWriteProjectFileStub).to.have.been.calledOnce; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.env", + `a=pikachu`, + false, + ); + }); + }); + + describe(`${manifest.writeLocalSecrets.name}`, () => { + let askWriteProjectFileStub: sinon.SinonStub; + + beforeEach(() => { + askWriteProjectFileStub = sandbox.stub(Config.prototype, "askWriteProjectFile"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should write all secret params that have local values", async () => { + await manifest.writeLocalSecrets( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { + a: { baseValue: "base", local: "pikachu" }, + b: { baseValue: "base", local: "bulbasaur" }, + }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.SECRET, + }, + { + param: "b", + label: "", + type: ParamType.SECRET, + }, + ], + systemParams: [], + }, + }, + { + instanceId: "instance-2", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "2.0.0", + }, + params: { + a: { baseValue: "base", local: "eevee" }, + b: { baseValue: "base", local: "squirtle" }, + }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.SECRET, + }, + { + param: "b", + label: "", + type: ParamType.SECRET, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + true, + ); + + expect(askWriteProjectFileStub).to.have.been.calledTwice; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.secret.local", + `a=pikachu\nb=bulbasaur`, + true, + ); + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-2.secret.local", + `a=eevee\nb=squirtle`, + true, + ); + }); + + it("should write only secret with local values", async () => { + await manifest.writeLocalSecrets( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { + a: { baseValue: "base", local: "pikachu" }, + b: { baseValue: "base" }, + }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.SECRET, + }, + { + param: "b", + label: "", + type: ParamType.SECRET, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + true, + ); + + expect(askWriteProjectFileStub).to.have.been.calledOnce; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.secret.local", + `a=pikachu`, + true, + ); + }); + + it("should write only local values that are ParamType.SECRET", async () => { + await manifest.writeLocalSecrets( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { + a: { baseValue: "base", local: "pikachu" }, + b: { baseValue: "base", local: "bulbasaur" }, + }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.SECRET, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + true, + ); + + expect(askWriteProjectFileStub).to.have.been.calledOnce; + expect(askWriteProjectFileStub).calledWithExactly( + "extensions/instance-1.secret.local", + `a=pikachu`, + true, + ); + }); + + it("should not write the file if there's no matching params", async () => { + await manifest.writeLocalSecrets( + [ + { + instanceId: "instance-1", + ref: { + publisherId: "firebase", + extensionId: "bigquery-export", + version: "1.0.0", + }, + params: { + // No local values + a: { baseValue: "base" }, + b: { baseValue: "base" }, + }, + extensionSpec: { + name: "bigquery-export", + version: "1.0.0", + resources: [], + sourceUrl: "", + params: [ + { + param: "a", + label: "", + type: ParamType.SECRET, + }, + { + param: "b", + label: "", + type: ParamType.STRING, + }, + ], + systemParams: [], + }, + }, + ], + generateBaseConfig(), + true, + ); + + expect(askWriteProjectFileStub).to.not.have.been.called; + }); + }); + + describe("readParams", () => { + let readEnvFileStub: sinon.SinonStub; + const testProjectDir = "test"; + const testProjectId = "my-project"; + const testProjectNumber = "123456"; + const testInstanceId = "extensionId"; + + beforeEach(() => { + readEnvFileStub = sinon.stub(paramHelper, "readEnvFile").returns({}); + }); + + afterEach(() => { + readEnvFileStub.restore(); + }); + + it("should read from generic .env file", () => { + readEnvFileStub + .withArgs("test/extensions/extensionId.env") + .returns({ param: "otherValue", param2: "value2" }); + + expect( + manifest.readInstanceParam({ + projectDir: testProjectDir, + instanceId: testInstanceId, + projectId: testProjectId, + projectNumber: testProjectNumber, + aliases: [], + }), + ).to.deep.equal({ param: "otherValue", param2: "value2" }); + }); + + it("should read from project id .env file", () => { + readEnvFileStub + .withArgs("test/extensions/extensionId.env.my-project") + .returns({ param: "otherValue", param2: "value2" }); + + expect( + manifest.readInstanceParam({ + projectDir: testProjectDir, + instanceId: testInstanceId, + projectId: testProjectId, + projectNumber: testProjectNumber, + aliases: [], + }), + ).to.deep.equal({ param: "otherValue", param2: "value2" }); + }); + + it("should read from project number .env file", () => { + readEnvFileStub + .withArgs("test/extensions/extensionId.env.123456") + .returns({ param: "otherValue", param2: "value2" }); + + expect( + manifest.readInstanceParam({ + projectDir: testProjectDir, + instanceId: testInstanceId, + projectId: testProjectId, + projectNumber: testProjectNumber, + aliases: [], + }), + ).to.deep.equal({ param: "otherValue", param2: "value2" }); + }); + + it("should read from an alias .env file", () => { + readEnvFileStub + .withArgs("test/extensions/extensionId.env.prod") + .returns({ param: "otherValue", param2: "value2" }); + + expect( + manifest.readInstanceParam({ + projectDir: testProjectDir, + instanceId: testInstanceId, + projectId: testProjectId, + projectNumber: testProjectNumber, + aliases: ["prod"], + }), + ).to.deep.equal({ param: "otherValue", param2: "value2" }); + }); + + it("should prefer values from project specific env files", () => { + readEnvFileStub + .withArgs("test/extensions/extensionId.env.my-project") + .returns({ param: "value" }); + readEnvFileStub + .withArgs("test/extensions/extensionId.env") + .returns({ param: "otherValue", param2: "value2" }); + + expect( + manifest.readInstanceParam({ + projectDir: testProjectDir, + instanceId: testInstanceId, + projectId: testProjectId, + projectNumber: testProjectNumber, + aliases: [], + }), + ).to.deep.equal({ param: "value", param2: "value2" }); + }); + }); +}); diff --git a/src/extensions/manifest.ts b/src/extensions/manifest.ts new file mode 100644 index 00000000000..cdcfc506ea4 --- /dev/null +++ b/src/extensions/manifest.ts @@ -0,0 +1,292 @@ +import * as clc from "colorette"; +import * as path from "path"; +import * as fs from "fs-extra"; + +import * as refs from "./refs"; +import { Config } from "../config"; +import { getExtensionSpec, ManifestInstanceSpec } from "../deploy/extensions/planner"; +import { logger } from "../logger"; +import { confirm, promptOnce } from "../prompt"; +import { readEnvFile } from "./paramHelper"; +import { FirebaseError } from "../error"; +import * as utils from "../utils"; +import { isLocalPath } from "./extensionsHelper"; +import { ParamType } from "./types"; + +export const ENV_DIRECTORY = "extensions"; + +/** + * Write a list of instanceSpecs to extensions manifest. + * + * The manifest is composed of both the extension instance list in firebase.json, and + * env-var for each extension instance under ./extensions/*.env + * + * @param specs a list of InstanceSpec to write to the manifest + * @param config existing config in firebase.json + * @param options.nonInteractive will try to do the job without asking for user input. + * @param options.force only when this flag is true this will overwrite existing .env files + * @param allowOverwrite allows overwriting the entire manifest with the new specs + */ +export async function writeToManifest( + specs: ManifestInstanceSpec[], + config: Config, + options: { nonInteractive: boolean; force: boolean }, + allowOverwrite: boolean = false, +): Promise { + if ( + config.has("extensions") && + Object.keys(config.get("extensions")).length && + !options.nonInteractive && + !options.force + ) { + const currentExtensions = Object.entries(config.get("extensions")) + .map((i) => `${i[0]}: ${i[1]}`) + .join("\n\t"); + if (allowOverwrite) { + const overwrite = await promptOnce({ + type: "list", + message: `firebase.json already contains extensions:\n${currentExtensions}\nWould you like to overwrite or merge?`, + choices: [ + { name: "Overwrite", value: true }, + { name: "Merge", value: false }, + ], + }); + if (overwrite) { + config.set("extensions", {}); + } + } + } + + writeExtensionsToFirebaseJson(specs, config); + await writeEnvFiles(specs, config, options.force); + await writeLocalSecrets(specs, config, options.force); +} + +export async function writeEmptyManifest( + config: Config, + options: { nonInteractive: boolean; force: boolean }, +): Promise { + if (!fs.existsSync(config.path("extensions"))) { + fs.mkdirSync(config.path("extensions")); + } + if (config.has("extensions") && Object.keys(config.get("extensions")).length) { + const currentExtensions = Object.entries(config.get("extensions")) + .map((i) => `${i[0]}: ${i[1]}`) + .join("\n\t"); + if ( + !(await confirm({ + message: `firebase.json already contains extensions:\n${currentExtensions}\nWould you like to overwrite them?`, + nonInteractive: options.nonInteractive, + force: options.force, + default: false, + })) + ) { + return; + } + } + config.set("extensions", {}); +} + +/** + * Write the secrets in a list of ManifestInstanceSpec into extensions/{instance-id}.secret.local. + * + * Exported for testing. + */ +export async function writeLocalSecrets( + specs: ManifestInstanceSpec[], + config: Config, + force?: boolean, +): Promise { + for (const spec of specs) { + const extensionSpec = await getExtensionSpec(spec); + if (!extensionSpec.params) { + continue; + } + + const writeBuffer: Record = {}; + const locallyOverridenSecretParams = extensionSpec.params.filter( + (p) => p.type === ParamType.SECRET && spec.params[p.param]?.local, + ); + for (const paramSpec of locallyOverridenSecretParams) { + const key = paramSpec.param; + const localValue = spec.params[key].local!; + writeBuffer[key] = localValue; + } + + const content = Object.entries(writeBuffer) + .sort((a, b) => { + return a[0].localeCompare(b[0]); + }) + .map((r) => `${r[0]}=${r[1]}`) + .join("\n"); + if (content) { + await config.askWriteProjectFile( + `extensions/${spec.instanceId}.secret.local`, + content, + force, + ); + } + } +} + +/** + * Remove an instance from extensions manifest. + */ +export function removeFromManifest(instanceId: string, config: Config) { + if (!instanceExists(instanceId, config)) { + throw new FirebaseError(`Extension instance ${instanceId} not found in firebase.json.`); + } + + const extensions = config.get("extensions", {}); + extensions[instanceId] = undefined; + config.set("extensions", extensions); + config.writeProjectFile("firebase.json", config.src); + logger.info(`Removed extension instance ${instanceId} from firebase.json`); + + config.deleteProjectFile(`extensions/${instanceId}.env`); + logger.info(`Removed extension instance environment config extensions/${instanceId}.env`); + if (config.projectFileExists(`extensions/${instanceId}.env.local`)) { + config.deleteProjectFile(`extensions/${instanceId}.env.local`); + logger.info( + `Removed extension instance local environment config extensions/${instanceId}.env.local`, + ); + } + if (config.projectFileExists(`extensions/${instanceId}.secret.local`)) { + config.deleteProjectFile(`extensions/${instanceId}.secret.local`); + logger.info( + `Removed extension instance local secret config extensions/${instanceId}.secret.local`, + ); + } + // TODO(lihes): Remove all project specific env files. +} + +export function loadConfig(options: any): Config { + const existingConfig = Config.load(options, true); + if (!existingConfig) { + throw new FirebaseError( + "Not currently in a Firebase directory. Run `firebase init` to create a Firebase directory.", + ); + } + return existingConfig; +} + +/** + * Checks if an instance name already exists in the manifest. + */ +export function instanceExists(instanceId: string, config: Config): boolean { + return !!config.get("extensions", {})[instanceId]; +} + +/** + * Gets the instance's extension ref string or local path given an instanceId. + */ +export function getInstanceTarget(instanceId: string, config: Config): string { + if (!instanceExists(instanceId, config)) { + throw new FirebaseError(`Could not find extension instance ${instanceId} in firebase.json`); + } + return config.get("extensions", {})[instanceId]; +} + +/** + * Gets the instance's extension ref if exists. + */ +export function getInstanceRef(instanceId: string, config: Config): refs.Ref { + const source = getInstanceTarget(instanceId, config); + if (isLocalPath(source)) { + throw new FirebaseError( + `Extension instance ${instanceId} doesn't have a ref because it is from a local source`, + ); + } + return refs.parse(source); +} + +export function writeExtensionsToFirebaseJson(specs: ManifestInstanceSpec[], config: Config): void { + const extensions = config.get("extensions", {}); + for (const s of specs) { + let target; + if (s.ref) { + target = refs.toExtensionVersionRef(s.ref!); + } else if (s.localPath) { + target = s.localPath; + } else { + throw new FirebaseError( + `Unable to resolve ManifestInstanceSpec, make sure you provide either extension ref or a local path to extension source code`, + ); + } + + extensions[s.instanceId] = target; + } + config.set("extensions", extensions); + config.writeProjectFile("firebase.json", config.src); + utils.logSuccess("Wrote extensions to " + clc.bold("firebase.json") + "..."); +} + +async function writeEnvFiles( + specs: ManifestInstanceSpec[], + config: Config, + force?: boolean, +): Promise { + for (const spec of specs) { + const content = Object.entries(spec.params) + .filter((r) => r[1].baseValue !== "" && r[1].baseValue !== undefined) // Don't write empty values + .sort((a, b) => { + return a[0].localeCompare(b[0]); + }) + .map((r) => `${r[0]}=${r[1].baseValue}`) + .join("\n"); + await config.askWriteProjectFile(`extensions/${spec.instanceId}.env`, content, force); + } +} + +/** + * readParams gets the params for an extension instance from the `extensions` folder, + * checking for project specific env files, then falling back to generic env files. + * This checks the following locations & if a param is defined in multiple places, it prefers + * whichever is higher on this list: + * - extensions/{instanceId}.env.local (only if checkLocal is true) + * - extensions/{instanceId}.env.{projectID} + * - extensions/{instanceId}.env.{projectNumber} + * - extensions/{instanceId}.env.{projectAlias} + * - extensions/{instanceId}.env + */ +export function readInstanceParam(args: { + instanceId: string; + projectDir: string; + projectId?: string; + projectNumber?: string; + aliases?: string[]; + checkLocal?: boolean; +}): Record { + const aliases = args.aliases ?? []; + const filesToCheck = [ + `${args.instanceId}.env`, + ...aliases.map((alias) => `${args.instanceId}.env.${alias}`), + ...(args.projectNumber ? [`${args.instanceId}.env.${args.projectNumber}`] : []), + ...(args.projectId ? [`${args.instanceId}.env.${args.projectId}`] : []), + ]; + if (args.checkLocal) { + filesToCheck.push(`${args.instanceId}.env.local`); + } + let noFilesFound = true; + const combinedParams = {}; + for (const fileToCheck of filesToCheck) { + try { + const params = readParamsFile(args.projectDir, fileToCheck); + logger.debug(`Successfully read params from ${fileToCheck}`); + noFilesFound = false; + Object.assign(combinedParams, params); + } catch (err: any) { + logger.debug(`${err}`); + } + } + if (noFilesFound) { + throw new FirebaseError(`No params file found for ${args.instanceId}`); + } + return combinedParams; +} + +function readParamsFile(projectDir: string, fileName: string): Record { + const paramPath = path.join(projectDir, ENV_DIRECTORY, fileName); + const params = readEnvFile(paramPath); + return params; +} diff --git a/src/extensions/metricsTypeDef.ts b/src/extensions/metricsTypeDef.ts new file mode 100644 index 00000000000..4e0a5c6f100 --- /dev/null +++ b/src/extensions/metricsTypeDef.ts @@ -0,0 +1,32 @@ +import * as refs from "./refs"; + +/** + * Interface for representing a metric to be rendered by the extension's CLI. + */ +export interface BucketedMetric { + ref: refs.Ref; + valueToday: Bucket | undefined; + value7dAgo: Bucket | undefined; + value28dAgo: Bucket | undefined; +} + +/** + * Bucket is the range that a raw number falls under. + * + * Valid bucket sizes are: + * 0 + * 0 - 10 + * 10 - 20 + * 20 - 30 + * ... + * 90 - 100 + * 100 - 200 + * 200 - 300 + * every 100... + * + * Note the buckets overlaps intentionally as a UX-optimization. + */ +export interface Bucket { + low: number; + high: number; +} diff --git a/src/extensions/metricsUtils.spec.ts b/src/extensions/metricsUtils.spec.ts new file mode 100644 index 00000000000..e2efac279ef --- /dev/null +++ b/src/extensions/metricsUtils.spec.ts @@ -0,0 +1,441 @@ +import { expect } from "chai"; +import * as clc from "colorette"; + +import { buildMetricsTableRow, parseBucket, parseTimeseriesResponse } from "./metricsUtils"; +import { TimeSeriesResponse, MetricKind, ValueType } from "../gcp/cloudmonitoring"; +import { BucketedMetric } from "./metricsTypeDef"; + +describe("metricsUtil", () => { + describe(`${parseBucket.name}`, () => { + it("should parse a bucket based on the higher bound value", () => { + expect(parseBucket(10)).to.deep.equals({ low: 0, high: 10 }); + expect(parseBucket(50)).to.deep.equals({ low: 40, high: 50 }); + expect(parseBucket(200)).to.deep.equals({ low: 100, high: 200 }); + expect(parseBucket(2200)).to.deep.equals({ low: 2100, high: 2200 }); + expect(parseBucket(0)).to.deep.equals({ low: 0, high: 0 }); + }); + }); + + describe("buildMetricsTableRow", () => { + it("shows decreasing instance count properly", () => { + const metric: BucketedMetric = { + ref: { + publisherId: "firebase", + extensionId: "bq-export", + version: "0.0.1", + }, + valueToday: { + high: 500, + low: 400, + }, + value7dAgo: { + high: 400, + low: 300, + }, + value28dAgo: { + high: 200, + low: 100, + }, + }; + expect(buildMetricsTableRow(metric)).to.deep.equals([ + "0.0.1", + "400 - 500", + clc.green("▲ ") + "100 (±100)", + clc.green("▲ ") + "300 (±100)", + ]); + }); + it("shows decreasing instance count properly", () => { + const metric: BucketedMetric = { + ref: { + publisherId: "firebase", + extensionId: "bq-export", + version: "0.0.1", + }, + valueToday: { + high: 200, + low: 100, + }, + value7dAgo: { + high: 200, + low: 100, + }, + value28dAgo: { + high: 300, + low: 200, + }, + }; + expect(buildMetricsTableRow(metric)).to.deep.equals([ + "0.0.1", + "100 - 200", + "-", + clc.red("▼ ") + "-100 (±100)", + ]); + }); + }); + + describe(`${parseTimeseriesResponse.name}`, () => { + it("should parse TimeSeriesResponse into list of BucketedMetrics", () => { + const series: TimeSeriesResponse = [ + { + metric: { + type: "firebaseextensions.googleapis.com/extension/version/active_instances", + labels: { + extension: "export-bigquery", + publisher: "firebase", + version: "0.1.0", + }, + }, + metricKind: MetricKind.GAUGE, + resource: { + labels: { + extension: "export-bigquery", + publisher: "firebase", + version: "all", + }, + type: "firebaseextensions.googleapis.com/ExtensionVersion", + }, + valueType: ValueType.INT64, + points: [ + { + interval: { + startTime: "2021-10-30T17:56:21.027Z", + endTime: "2021-10-30T17:56:21.027Z", + }, + value: { + int64Value: 10, + }, + }, + ], + }, + { + metric: { + type: "firebaseextensions.googleapis.com/extension/version/active_instances", + labels: { + extension: "export-bigquery", + publisher: "firebase", + version: "0.1.0", + }, + }, + metricKind: MetricKind.GAUGE, + resource: { + labels: { + extension: "export-bigquery", + publisher: "firebase", + version: "0.1.0", + }, + type: "firebaseextensions.googleapis.com/ExtensionVersion", + }, + valueType: ValueType.INT64, + points: [ + { + interval: { + startTime: "2021-10-30T17:56:21.027Z", + endTime: "2021-10-30T17:56:21.027Z", + }, + value: { + int64Value: 10, + }, + }, + { + interval: { + startTime: "2021-10-29T17:56:21.027Z", + endTime: "2021-10-29T17:56:21.027Z", + }, + value: { + int64Value: 20, + }, + }, + { + interval: { + startTime: "2021-10-28T17:56:21.027Z", + endTime: "2021-10-28T17:56:21.027Z", + }, + value: { + int64Value: 30, + }, + }, + { + interval: { + startTime: "2021-10-27T17:56:21.027Z", + endTime: "2021-10-27T17:56:21.027Z", + }, + value: { + int64Value: 40, + }, + }, + { + interval: { + startTime: "2021-10-26T17:56:21.027Z", + endTime: "2021-10-26T17:56:21.027Z", + }, + value: { + int64Value: 50, + }, + }, + { + interval: { + startTime: "2021-10-25T17:56:21.027Z", + endTime: "2021-10-25T17:56:21.027Z", + }, + value: { + int64Value: 60, + }, + }, + { + interval: { + startTime: "2021-10-24T17:56:21.027Z", + endTime: "2021-10-24T17:56:21.027Z", + }, + value: { + int64Value: 70, + }, + }, + { + interval: { + startTime: "2021-10-23T17:56:21.027Z", + endTime: "2021-10-23T17:56:21.027Z", + }, + value: { + int64Value: 80, + }, + }, + { + interval: { + startTime: "2021-10-22T17:56:21.027Z", + endTime: "2021-10-22T17:56:21.027Z", + }, + value: { + int64Value: 90, + }, + }, + { + interval: { + startTime: "2021-10-21T17:56:21.027Z", + endTime: "2021-10-21T17:56:21.027Z", + }, + value: { + int64Value: 100, + }, + }, + { + interval: { + startTime: "2021-10-20T17:56:21.027Z", + endTime: "2021-10-20T17:56:21.027Z", + }, + value: { + int64Value: 200, + }, + }, + { + interval: { + startTime: "2021-10-19T17:56:21.027Z", + endTime: "2021-10-19T17:56:21.027Z", + }, + value: { + int64Value: 300, + }, + }, + { + interval: { + startTime: "2021-10-18T17:56:21.027Z", + endTime: "2021-10-18T17:56:21.027Z", + }, + value: { + int64Value: 400, + }, + }, + { + interval: { + startTime: "2021-10-17T17:56:21.027Z", + endTime: "2021-10-17T17:56:21.027Z", + }, + value: { + int64Value: 500, + }, + }, + { + interval: { + startTime: "2021-10-16T17:56:21.027Z", + endTime: "2021-10-16T17:56:21.027Z", + }, + value: { + int64Value: 600, + }, + }, + { + interval: { + startTime: "2021-10-15T17:56:21.027Z", + endTime: "2021-10-15T17:56:21.027Z", + }, + value: { + int64Value: 700, + }, + }, + { + interval: { + startTime: "2021-10-14T17:56:21.027Z", + endTime: "2021-10-14T17:56:21.027Z", + }, + value: { + int64Value: 800, + }, + }, + { + interval: { + startTime: "2021-10-13T17:56:21.027Z", + endTime: "2021-10-13T17:56:21.027Z", + }, + value: { + int64Value: 900, + }, + }, + { + interval: { + startTime: "2021-10-12T17:56:21.027Z", + endTime: "2021-10-12T17:56:21.027Z", + }, + value: { + int64Value: 1000, + }, + }, + { + interval: { + startTime: "2021-10-11T17:56:21.027Z", + endTime: "2021-10-11T17:56:21.027Z", + }, + value: { + int64Value: 1100, + }, + }, + { + interval: { + startTime: "2021-10-10T17:56:21.027Z", + endTime: "2021-10-10T17:56:21.027Z", + }, + value: { + int64Value: 1200, + }, + }, + { + interval: { + startTime: "2021-10-09T17:56:21.027Z", + endTime: "2021-10-09T17:56:21.027Z", + }, + value: { + int64Value: 1300, + }, + }, + { + interval: { + startTime: "2021-10-08T17:56:21.027Z", + endTime: "2021-10-08T17:56:21.027Z", + }, + value: { + int64Value: 1400, + }, + }, + { + interval: { + startTime: "2021-10-07T17:56:21.027Z", + endTime: "2021-10-07T17:56:21.027Z", + }, + value: { + int64Value: 1500, + }, + }, + { + interval: { + startTime: "2021-10-06T17:56:21.027Z", + endTime: "2021-10-06T17:56:21.027Z", + }, + value: { + int64Value: 1600, + }, + }, + { + interval: { + startTime: "2021-10-05T17:56:21.027Z", + endTime: "2021-10-05T17:56:21.027Z", + }, + value: { + int64Value: 1700, + }, + }, + { + interval: { + startTime: "2021-10-04T17:56:21.027Z", + endTime: "2021-10-04T17:56:21.027Z", + }, + value: { + int64Value: 1800, + }, + }, + { + interval: { + startTime: "2021-10-03T17:56:21.027Z", + endTime: "2021-10-03T17:56:21.027Z", + }, + value: { + int64Value: 1900, + }, + }, + { + interval: { + startTime: "2021-10-02T17:56:21.027Z", + endTime: "2021-10-02T17:56:21.027Z", + }, + value: { + int64Value: 2000, + }, + }, + { + interval: { + startTime: "2021-10-01T17:56:21.027Z", + endTime: "2021-10-01T17:56:21.027Z", + }, + value: { + int64Value: 2100, + }, + }, + ], + }, + ]; + + expect(parseTimeseriesResponse(series)).to.deep.equals([ + { + ref: { + extensionId: "export-bigquery", + publisherId: "firebase", + version: "0.1.0", + }, + value28dAgo: { + high: 1900, + low: 1800, + }, + value7dAgo: { + high: 70, + low: 60, + }, + valueToday: { + high: 10, + low: 0, + }, + }, + // Should sort "all" to the end. + { + ref: { + extensionId: "export-bigquery", + publisherId: "firebase", + version: "all", + }, + value28dAgo: undefined, + value7dAgo: undefined, + valueToday: { + high: 10, + low: 0, + }, + }, + ]); + }); + }); +}); diff --git a/src/extensions/metricsUtils.ts b/src/extensions/metricsUtils.ts new file mode 100644 index 00000000000..c6bb1ab8c03 --- /dev/null +++ b/src/extensions/metricsUtils.ts @@ -0,0 +1,134 @@ +import * as semver from "semver"; +import { TimeSeries, TimeSeriesResponse } from "../gcp/cloudmonitoring"; +import { Bucket, BucketedMetric } from "./metricsTypeDef"; +import * as refs from "./refs"; +import * as clc from "colorette"; + +/** + * Parse TimeSeriesResponse into structured metric data. + */ +export function parseTimeseriesResponse(series: TimeSeriesResponse): Array { + const ret: BucketedMetric[] = []; + for (const s of series) { + const ref = buildRef(s); + + if (ref === undefined) { + // Skip if data point has no valid ref. + continue; + } + + let valueToday: Bucket | undefined; + let value7dAgo: Bucket | undefined; + let value28dAgo: Bucket | undefined; + + // Extract significant data points and convert them to buckets. + if (s.points.length >= 28 && s.points[27].value.int64Value !== undefined) { + value28dAgo = parseBucket(s.points[27].value.int64Value); + } + if (s.points.length >= 7 && s.points[6].value.int64Value !== undefined) { + value7dAgo = parseBucket(s.points[6].value.int64Value); + } + if (s.points.length >= 1 && s.points[0].value.int64Value !== undefined) { + valueToday = parseBucket(s.points[0].value.int64Value); + } + + ret.push({ + ref, + valueToday, + value7dAgo, + value28dAgo, + }); + } + + ret.sort((a, b) => { + if (a.ref.version === "all") { + return 1; + } + if (b.ref.version === "all") { + return -1; + } + return semver.lt(a.ref.version!, b.ref.version!) ? 1 : -1; + }); + return ret; +} + +/** + * Converts a single number back into a range bucket that the raw number falls under. + * + * The reverse side of the logic lives here: + * https://source.corp.google.com/piper///depot/google3/firebase/mods/jobs/metrics/buckets.go + * + * @param v Value got from Cloud Monitoring, which is the upper-bound of the bucket. + */ +export function parseBucket(value: number): Bucket { + // int64Value has type "number" but can still be interupted as "string" sometimes. + // Force cast into number just in case. + const v = Number(value); + + if (v >= 200) { + return { low: v - 100, high: v }; + } + if (v >= 10) { + return { low: v - 10, high: v }; + } + return { low: 0, high: 0 }; +} + +/** + * Build a row in the metrics table given a bucketed metric. + */ +export function buildMetricsTableRow(metric: BucketedMetric): Array { + const ret: string[] = [metric.ref.version!]; + + if (metric.valueToday) { + ret.push(`${metric.valueToday.low} - ${metric.valueToday.high}`); + } else { + ret.push("Insufficient data"); + } + + ret.push(renderChangeCell(metric.value7dAgo, metric.valueToday)); + + ret.push(renderChangeCell(metric.value28dAgo, metric.valueToday)); + + return ret; +} + +function renderChangeCell(before: Bucket | undefined, after: Bucket | undefined) { + if (!(before && after)) { + return "Insufficient data"; + } + if (before.high === after.high) { + return "-"; + } + + if (before.high > after.high) { + const diff = before.high - after.high; + const tolerance = diff < 100 ? 10 : 100; + return clc.red("▼ ") + `-${diff} (±${tolerance})`; + } else { + const diff = after.high - before.high; + const tolerance = diff < 100 ? 10 : 100; + return clc.green("▲ ") + `${diff} (±${tolerance})`; + } +} + +/** + * Build an extension ref from a Cloud Monitoring's TimeSeries. + * + * Return null if resource labels are malformed. + */ +function buildRef(ts: TimeSeries): refs.Ref | undefined { + const publisherId = ts.resource.labels["publisher"]; + const extensionId = ts.resource.labels["extension"]; + const version = ts.resource.labels["version"]; + + if (!(publisherId && extensionId && version)) { + return undefined; + } + + return { + publisherId, + extensionId, + version, + }; +} diff --git a/src/extensions/paramHelper.spec.ts b/src/extensions/paramHelper.spec.ts new file mode 100644 index 00000000000..660a502e3a6 --- /dev/null +++ b/src/extensions/paramHelper.spec.ts @@ -0,0 +1,377 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as fs from "fs-extra"; + +import { FirebaseError } from "../error"; +import { ExtensionSpec, Param, ParamType } from "./types"; +import * as extensionsHelper from "./extensionsHelper"; +import * as paramHelper from "./paramHelper"; +import * as prompt from "../prompt"; +import { cloneDeep } from "../utils"; + +const PROJECT_ID = "test-proj"; +const INSTANCE_ID = "ext-instance"; +const TEST_PARAMS: Param[] = [ + { + param: "A_PARAMETER", + label: "Param", + type: ParamType.STRING, + required: true, + }, + { + param: "ANOTHER_PARAMETER", + label: "Another Param", + default: "default", + type: ParamType.STRING, + required: true, + }, +]; + +const TEST_PARAMS_2: Param[] = [ + { + param: "ANOTHER_PARAMETER", + label: "Another Param", + type: ParamType.STRING, + default: "default", + }, + { + param: "NEW_PARAMETER", + label: "New Param", + type: ParamType.STRING, + default: "${PROJECT_ID}", + }, + { + param: "THIRD_PARAMETER", + label: "3", + type: ParamType.STRING, + default: "default", + }, +]; +const TEST_PARAMS_3: Param[] = [ + { + param: "A_PARAMETER", + label: "Param", + type: ParamType.STRING, + }, + { + param: "ANOTHER_PARAMETER", + label: "Another Param", + default: "default", + type: ParamType.STRING, + description: "Something new", + required: false, + }, +]; + +const SPEC: ExtensionSpec = { + name: "test", + version: "0.1.0", + roles: [], + resources: [], + sourceUrl: "test.com", + params: TEST_PARAMS, + systemParams: [], +}; + +describe("paramHelper", () => { + describe(`${paramHelper.getBaseParamBindings.name}`, () => { + it("should extract the baseValue param bindings", () => { + const input = { + pokeball: { + baseValue: "pikachu", + local: "local", + }, + greatball: { + baseValue: "eevee", + }, + }; + const output = paramHelper.getBaseParamBindings(input); + expect(output).to.eql({ + pokeball: "pikachu", + greatball: "eevee", + }); + }); + }); + + describe(`${paramHelper.buildBindingOptionsWithBaseValue.name}`, () => { + it("should build given baseValue values", () => { + const input = { + pokeball: "pikachu", + greatball: "eevee", + }; + const output = paramHelper.buildBindingOptionsWithBaseValue(input); + expect(output).to.eql({ + pokeball: { + baseValue: "pikachu", + }, + greatball: { + baseValue: "eevee", + }, + }); + }); + }); + + describe("getParams", () => { + let promptStub: sinon.SinonStub; + + beforeEach(() => { + sinon.stub(fs, "readFileSync").returns(""); + sinon.stub(extensionsHelper, "getFirebaseProjectParams").resolves({ PROJECT_ID }); + promptStub = sinon.stub(prompt, "promptOnce").resolves("user input"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should prompt the user for params", async () => { + const params = await paramHelper.getParams({ + projectId: PROJECT_ID, + paramSpecs: TEST_PARAMS, + instanceId: INSTANCE_ID, + }); + + expect(params).to.eql({ + A_PARAMETER: { baseValue: "user input" }, + ANOTHER_PARAMETER: { baseValue: "user input" }, + }); + + expect(promptStub).to.have.been.calledTwice; + expect(promptStub.firstCall.args[0]).to.eql({ + default: undefined, + message: "Enter a value for Param:", + name: "A_PARAMETER", + type: "input", + }); + expect(promptStub.secondCall.args[0]).to.eql({ + default: "default", + message: "Enter a value for Another Param:", + name: "ANOTHER_PARAMETER", + type: "input", + }); + }); + }); + + describe("promptForNewParams", () => { + let promptStub: sinon.SinonStub; + + beforeEach(() => { + promptStub = sinon.stub(prompt, "promptOnce"); + sinon.stub(extensionsHelper, "getFirebaseProjectParams").resolves({ PROJECT_ID }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should prompt the user for any params in the new spec that are not in the current one", async () => { + promptStub.resolves("user input"); + const newSpec = cloneDeep(SPEC); + newSpec.params = TEST_PARAMS_2; + + const newParams = await paramHelper.promptForNewParams({ + spec: SPEC, + newSpec, + currentParams: { + A_PARAMETER: "value", + ANOTHER_PARAMETER: "value", + }, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + }); + + const expected = { + ANOTHER_PARAMETER: { baseValue: "value" }, + NEW_PARAMETER: { baseValue: "user input" }, + THIRD_PARAMETER: { baseValue: "user input" }, + }; + expect(newParams).to.eql(expected); + expect(promptStub.callCount).to.equal(2); + expect(promptStub.firstCall.args).to.eql([ + { + default: "test-proj", + message: "Enter a value for New Param:", + name: "NEW_PARAMETER", + type: "input", + }, + ]); + expect(promptStub.secondCall.args).to.eql([ + { + default: "default", + message: "Enter a value for 3:", + name: "THIRD_PARAMETER", + type: "input", + }, + ]); + }); + + it("should prompt for params that are not currently populated", async () => { + promptStub.resolves("user input"); + const newSpec = cloneDeep(SPEC); + newSpec.params = TEST_PARAMS_2; + + const newParams = await paramHelper.promptForNewParams({ + spec: SPEC, + newSpec, + currentParams: { + A_PARAMETER: "value", + // ANOTHER_PARAMETER is not populated + }, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + }); + + const expected = { + ANOTHER_PARAMETER: { baseValue: "user input" }, + NEW_PARAMETER: { baseValue: "user input" }, + THIRD_PARAMETER: { baseValue: "user input" }, + }; + expect(newParams).to.eql(expected); + }); + + it("should map LOCATION to system param location and not prompt for it", async () => { + promptStub.resolves("user input"); + const oldSpec = cloneDeep(SPEC); + const newSpec = cloneDeep(SPEC); + oldSpec.params = [ + { + param: "LOCATION", + label: "", + }, + ]; + newSpec.params = []; + newSpec.systemParams = [ + { + param: "firebaseextensions.v1beta.function/location", + label: "", + }, + ]; + + const newParams = await paramHelper.promptForNewParams({ + spec: oldSpec, + newSpec, + currentParams: { + LOCATION: "us-east1", + }, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + }); + + const expected = { + "firebaseextensions.v1beta.function/location": { baseValue: "us-east1" }, + }; + expect(newParams).to.eql(expected); + expect(promptStub).not.to.have.been.called; + }); + + it("should not prompt the user for params that did not change type or param", async () => { + promptStub.resolves("Fail"); + const newSpec = cloneDeep(SPEC); + newSpec.params = TEST_PARAMS_3; + + const newParams = await paramHelper.promptForNewParams({ + spec: SPEC, + newSpec, + currentParams: { + A_PARAMETER: "value", + ANOTHER_PARAMETER: "value", + }, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + }); + + const expected = { + ANOTHER_PARAMETER: { baseValue: "value" }, + A_PARAMETER: { baseValue: "value" }, + }; + expect(newParams).to.eql(expected); + expect(promptStub).not.to.have.been.called; + }); + + it("should populate the spec with the default value if it is returned by prompt", async () => { + promptStub.onFirstCall().resolves("test-proj"); + promptStub.onSecondCall().resolves("user input"); + const newSpec = cloneDeep(SPEC); + newSpec.params = TEST_PARAMS_2; + + const newParams = await paramHelper.promptForNewParams({ + spec: SPEC, + newSpec, + currentParams: { + A_PARAMETER: "value", + ANOTHER_PARAMETER: "value", + }, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + }); + + const expected = { + ANOTHER_PARAMETER: { baseValue: "value" }, + NEW_PARAMETER: { baseValue: "test-proj" }, + THIRD_PARAMETER: { baseValue: "user input" }, + }; + expect(newParams).to.eql(expected); + expect(promptStub.callCount).to.equal(2); + expect(promptStub.firstCall.args).to.eql([ + { + default: "test-proj", + message: "Enter a value for New Param:", + name: "NEW_PARAMETER", + type: "input", + }, + ]); + expect(promptStub.secondCall.args).to.eql([ + { + default: "default", + message: "Enter a value for 3:", + name: "THIRD_PARAMETER", + type: "input", + }, + ]); + }); + + it("shouldn't prompt if there are no new params", async () => { + promptStub.resolves("Fail"); + const newSpec = cloneDeep(SPEC); + + const newParams = await paramHelper.promptForNewParams({ + spec: SPEC, + newSpec, + currentParams: { + A_PARAMETER: "value", + ANOTHER_PARAMETER: "value", + }, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + }); + + const expected = { + ANOTHER_PARAMETER: { baseValue: "value" }, + A_PARAMETER: { baseValue: "value" }, + }; + expect(newParams).to.eql(expected); + expect(promptStub).not.to.have.been.called; + }); + + it("should exit if a prompt fails", async () => { + promptStub.rejects(new FirebaseError("this is an error")); + const newSpec = cloneDeep(SPEC); + newSpec.params = TEST_PARAMS_2; + + await expect( + paramHelper.promptForNewParams({ + spec: SPEC, + newSpec, + currentParams: { + A_PARAMETER: "value", + ANOTHER_PARAMETER: "value", + }, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + }), + ).to.be.rejectedWith(FirebaseError, "this is an error"); + // Ensure that we don't continue prompting if one fails + expect(promptStub).to.have.been.calledOnce; + }); + }); +}); diff --git a/src/extensions/paramHelper.ts b/src/extensions/paramHelper.ts index 84ffb2848fd..729d24aca0c 100644 --- a/src/extensions/paramHelper.ts +++ b/src/extensions/paramHelper.ts @@ -1,21 +1,57 @@ -import * as _ from "lodash"; import * as path from "path"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs-extra"; import { FirebaseError } from "../error"; import { logger } from "../logger"; -import * as extensionsApi from "./extensionsApi"; -import { - getFirebaseProjectParams, - populateDefaultParams, - substituteParams, - validateCommandLineParams, -} from "./extensionsHelper"; +import { ExtensionSpec, Param } from "./types"; +import { getFirebaseProjectParams, substituteParams } from "./extensionsHelper"; import * as askUserForParam from "./askUserForParam"; -import * as track from "../track"; import * as env from "../functions/env"; +const NONINTERACTIVE_ERROR_MESSAGE = + "As of firebase-tools@11, `ext:install`, `ext:update` and `ext:configure` are interactive only commands. " + + "To deploy an extension noninteractively, use an extensions manifest and `firebase deploy --only extensions`. " + + "See https://firebase.google.com/docs/extensions/manifest for more details"; + +/** + * Interface for holding different param values for different environments/configs. + * + * baseValue: The base value of the configurations, stored in {instance-id}.env. + * local: The local value used by extensions emulators. Only used by secrets in {instance-id}.secret.env for now. + */ +export interface ParamBindingOptions { + baseValue: string; + local?: string; + // Add project specific key:value here when we want to support that. +} + +export function getBaseParamBindings(params: { [key: string]: ParamBindingOptions }): { + [key: string]: string; +} { + let ret = {}; + for (const [k, v] of Object.entries(params)) { + ret = { + ...ret, + ...{ [k]: v.baseValue }, + }; + } + return ret; +} + +export function buildBindingOptionsWithBaseValue(baseParams: { [key: string]: string }): { + [key: string]: ParamBindingOptions; +} { + let paramOptions: { [key: string]: ParamBindingOptions } = {}; + for (const [k, v] of Object.entries(baseParams)) { + paramOptions = { + ...paramOptions, + ...{ [k]: { baseValue: v } }, + }; + } + return paramOptions; +} + /** * A mutator to switch the defaults for a list of params to new ones. * For convenience, this also returns the params @@ -23,33 +59,22 @@ import * as env from "../functions/env"; * @param params A list of params * @param newDefaults a map of { PARAM_NAME: default_value } */ -function setNewDefaults( - params: extensionsApi.Param[], - newDefaults: { [key: string]: string } -): extensionsApi.Param[] { - params.forEach((param) => { - if (newDefaults[param.param.toUpperCase()]) { - param.default = newDefaults[param.param.toUpperCase()]; +export function setNewDefaults(params: Param[], newDefaults: { [key: string]: string }): Param[] { + for (const param of params) { + if (newDefaults[param.param]) { + param.default = newDefaults[param.param]; + } else if ( + (param.param = `firebaseextensions.v1beta.function/location` && newDefaults["LOCATION"]) + ) { + // Special case handling for when we are updating from LOCATION to system param location. + param.default = newDefaults["LOCATION"]; } - }); + } return params; } /** - * Returns a copy of the params for a extension instance with the defaults set to the instance's current param values - * @param extensionInstance the extension instance to change the default params of - */ -export function getParamsWithCurrentValuesAsDefaults( - extensionInstance: extensionsApi.ExtensionInstance -): extensionsApi.Param[] { - const specParams = _.cloneDeep(_.get(extensionInstance, "config.source.spec.params", [])); - const currentParams = _.cloneDeep(_.get(extensionInstance, "config.params", {})); - return setNewDefaults(specParams, currentParams); -} - -/** - * Gets params from the user, either by - * reading the env file passed in the --params command line option + * Gets params from the user * or prompting the user for each param. * @param projectId the id of the project in use * @param paramSpecs a list of params, ie. extensionSpec.params @@ -57,74 +82,39 @@ export function getParamsWithCurrentValuesAsDefaults( * @throws FirebaseError if an invalid env file is passed in */ export async function getParams(args: { - projectId: string; - paramSpecs: extensionsApi.Param[]; - nonInteractive?: boolean; - paramsEnvPath?: string; + projectId?: string; instanceId: string; + paramSpecs: Param[]; + nonInteractive?: boolean; reconfiguring?: boolean; -}): Promise<{ [key: string]: string }> { - let params: any; - if (args.nonInteractive && !args.paramsEnvPath) { - const paramsMessage = args.paramSpecs - .map((p) => { - return `\t${p.param}${p.required ? "" : " (Optional)"}`; - }) - .join("\n"); - throw new FirebaseError( - "In non-interactive mode but no `--params` flag found. " + - "To install this extension in non-interactive mode, set `--params` to a path to an .env file" + - " containing values for this extension's params:\n" + - paramsMessage - ); - } else if (args.paramsEnvPath) { - params = getParamsFromFile({ - projectId: args.projectId, - paramSpecs: args.paramSpecs, - paramsEnvPath: args.paramsEnvPath, - }); +}): Promise> { + let params: Record; + if (args.nonInteractive) { + throw new FirebaseError(NONINTERACTIVE_ERROR_MESSAGE); } else { const firebaseProjectParams = await getFirebaseProjectParams(args.projectId); - params = await askUserForParam.ask( - args.projectId, - args.instanceId, - args.paramSpecs, + params = await askUserForParam.ask({ + projectId: args.projectId, + instanceId: args.instanceId, + paramSpecs: args.paramSpecs, firebaseProjectParams, - !!args.reconfiguring - ); + reconfiguring: !!args.reconfiguring, + }); } - track("Extension Params", _.isEmpty(params) ? "Not Present" : "Present", _.size(params)); return params; } export async function getParamsForUpdate(args: { - spec: extensionsApi.ExtensionSpec; - newSpec: extensionsApi.ExtensionSpec; + spec: ExtensionSpec; + newSpec: ExtensionSpec; currentParams: { [option: string]: string }; - projectId: string; - paramsEnvPath?: string; + projectId?: string; nonInteractive?: boolean; instanceId: string; -}) { - let params: any; - if (args.nonInteractive && !args.paramsEnvPath) { - const paramsMessage = args.newSpec.params - .map((p) => { - return `\t${p.param}${p.required ? "" : " (Optional)"}`; - }) - .join("\n"); - throw new FirebaseError( - "In non-interactive mode but no `--params` flag found. " + - "To update this extension in non-interactive mode, set `--params` to a path to an .env file" + - " containing values for this extension's params:\n" + - paramsMessage - ); - } else if (args.paramsEnvPath) { - params = getParamsFromFile({ - projectId: args.projectId, - paramSpecs: args.newSpec.params, - paramsEnvPath: args.paramsEnvPath, - }); +}): Promise> { + let params: Record; + if (args.nonInteractive) { + throw new FirebaseError(NONINTERACTIVE_ERROR_MESSAGE); } else { params = await promptForNewParams({ spec: args.spec, @@ -134,7 +124,6 @@ export async function getParamsForUpdate(args: { instanceId: args.instanceId, }); } - track("Extension Params", _.isEmpty(params) ? "Not Present" : "Present", _.size(params)); return params; } @@ -147,86 +136,92 @@ export async function getParamsForUpdate(args: { * @param currentParams A set of current params and their values */ export async function promptForNewParams(args: { - spec: extensionsApi.ExtensionSpec; - newSpec: extensionsApi.ExtensionSpec; + spec: ExtensionSpec; + newSpec: ExtensionSpec; currentParams: { [option: string]: string }; - projectId: string; + projectId?: string; instanceId: string; -}): Promise { +}): Promise<{ [option: string]: ParamBindingOptions }> { + const newParamBindingOptions = buildBindingOptionsWithBaseValue(args.currentParams); + const firebaseProjectParams = await getFirebaseProjectParams(args.projectId); - const comparer = (param1: extensionsApi.Param, param2: extensionsApi.Param) => { + const sameParam = (param1: Param) => (param2: Param) => { return param1.type === param2.type && param1.param === param2.param; }; - let paramsDiffDeletions = _.differenceWith( - args.spec.params, - _.get(args.newSpec, "params", []), - comparer + const paramDiff = (left: Param[], right: Param[]): Param[] => { + return left.filter((aLeft) => !right.find(sameParam(aLeft))); + }; + + let combinedOldParams = args.spec.params.concat( + args.spec.systemParams.filter((p) => !p.advanced) ?? [], ); - paramsDiffDeletions = substituteParams( - paramsDiffDeletions, - firebaseProjectParams + let combinedNewParams = args.newSpec.params.concat( + args.newSpec.systemParams.filter((p) => !p.advanced) ?? [], ); - let paramsDiffAdditions = _.differenceWith( - args.newSpec.params, - _.get(args.spec, "params", []), - comparer - ); - paramsDiffAdditions = substituteParams( - paramsDiffAdditions, - firebaseProjectParams + // Special case for updating from LOCATION to system param location + if ( + combinedOldParams.some((p) => p.param === "LOCATION") && + combinedNewParams.some((p) => p.param === "firebaseextensions.v1beta.function/location") && + !!args.currentParams["LOCATION"] + ) { + newParamBindingOptions["firebaseextensions.v1beta.function/location"] = { + baseValue: args.currentParams["LOCATION"], + }; + delete newParamBindingOptions["LOCATION"]; + combinedOldParams = combinedOldParams.filter((p) => p.param !== "LOCATION"); + combinedNewParams = combinedNewParams.filter( + (p) => p.param !== "firebaseextensions.v1beta.function/location", + ); + } + + // Some params are in the spec but not in currentParams, remove so we can prompt for them. + const oldParams = combinedOldParams.filter((p) => + Object.keys(args.currentParams).includes(p.param), ); + let paramsDiffDeletions = paramDiff(oldParams, combinedNewParams); + paramsDiffDeletions = substituteParams(paramsDiffDeletions, firebaseProjectParams); + + let paramsDiffAdditions = paramDiff(combinedNewParams, oldParams); + paramsDiffAdditions = substituteParams(paramsDiffAdditions, firebaseProjectParams); if (paramsDiffDeletions.length) { logger.info("The following params will no longer be used:"); - paramsDiffDeletions.forEach((param) => { + for (const param of paramsDiffDeletions) { logger.info(clc.red(`- ${param.param}: ${args.currentParams[param.param.toUpperCase()]}`)); - delete args.currentParams[param.param.toUpperCase()]; - }); + delete newParamBindingOptions[param.param.toUpperCase()]; + } } if (paramsDiffAdditions.length) { logger.info("To update this instance, configure the following new parameters:"); for (const param of paramsDiffAdditions) { - const chosenValue = await askUserForParam.askForParam( - args.projectId, - args.instanceId, - param, - false - ); - args.currentParams[param.param] = chosenValue; + const chosenValue = await askUserForParam.askForParam({ + projectId: args.projectId, + instanceId: args.instanceId, + paramSpec: param, + reconfiguring: false, + }); + newParamBindingOptions[param.param] = chosenValue; } } - return args.currentParams; -} -export function getParamsFromFile(args: { - projectId: string; - paramSpecs: extensionsApi.Param[]; - paramsEnvPath: string; -}): Record { - let envParams; - try { - envParams = readEnvFile(args.paramsEnvPath); - track("Extension Env File", "Present"); - } catch (err) { - track("Extension Env File", "Invalid"); - throw new FirebaseError(`Error reading env file: ${err.message}\n`, { original: err }); - } - const params = populateDefaultParams(envParams, args.paramSpecs); - validateCommandLineParams(params, args.paramSpecs); - logger.info(`Using param values from ${args.paramsEnvPath}`); - return params; + return newParamBindingOptions; } -export function readEnvFile(envPath: string) { +export function readEnvFile(envPath: string): Record { const buf = fs.readFileSync(path.resolve(envPath), "utf8"); const result = env.parse(buf.toString().trim()); if (result.errors.length) { throw new FirebaseError( `Error while parsing ${envPath} - unable to parse following lines:\n${result.errors.join( - "\n" - )}` + "\n", + )}`, ); } return result.envs; } + +export function isSystemParam(paramName: string): boolean { + const regex = /^firebaseextensions\.[a-zA-Z0-9\.]*\//; + return regex.test(paramName); +} diff --git a/src/test/extensions/provisioningHelper.spec.ts b/src/extensions/provisioningHelper.spec.ts similarity index 80% rename from src/test/extensions/provisioningHelper.spec.ts rename to src/extensions/provisioningHelper.spec.ts index b62c80d47e8..df36f2dc7f8 100644 --- a/src/test/extensions/provisioningHelper.spec.ts +++ b/src/extensions/provisioningHelper.spec.ts @@ -1,36 +1,35 @@ import * as nock from "nock"; import { expect } from "chai"; -import * as api from "../../api"; -import * as provisioningHelper from "../../extensions/provisioningHelper"; -import * as extensionsApi from "../../extensions/extensionsApi"; -import { FirebaseError } from "../../error"; +import * as api from "../api"; +import * as provisioningHelper from "./provisioningHelper"; +import { Api, ExtensionSpec, Resource, Role } from "./types"; +import { FirebaseError } from "../error"; -const TEST_INSTANCES_RESPONSE = {}; const PROJECT_ID = "test-project"; const SPEC_WITH_NOTHING = { - apis: [] as extensionsApi.Api[], - resources: [] as extensionsApi.Resource[], -} as extensionsApi.ExtensionSpec; + apis: [] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; const SPEC_WITH_STORAGE = { apis: [ { apiName: "storage-component.googleapis.com", }, - ] as extensionsApi.Api[], - resources: [] as extensionsApi.Resource[], -} as extensionsApi.ExtensionSpec; + ] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; const SPEC_WITH_AUTH = { apis: [ { apiName: "identitytoolkit.googleapis.com", }, - ] as extensionsApi.Api[], - resources: [] as extensionsApi.Resource[], -} as extensionsApi.ExtensionSpec; + ] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; const SPEC_WITH_STORAGE_AND_AUTH = { apis: [ @@ -40,9 +39,9 @@ const SPEC_WITH_STORAGE_AND_AUTH = { { apiName: "identitytoolkit.googleapis.com", }, - ] as extensionsApi.Api[], - resources: [] as extensionsApi.Resource[], -} as extensionsApi.ExtensionSpec; + ] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; const FIREDATA_AUTH_ACTIVATED_RESPONSE = { activation: [ @@ -60,7 +59,7 @@ const FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE = { ], }; -const extensionVersionResponse = (version: string, spec: extensionsApi.ExtensionSpec) => { +const extensionVersionResponse = (version: string, spec: ExtensionSpec) => { return { name: `publishers/test/extensions/test/version/${version}`, ref: `test/test@${version}`, @@ -88,7 +87,7 @@ describe("provisioningHelper", () => { }); describe("getUsedProducts", () => { - let testSpec: extensionsApi.ExtensionSpec; + let testSpec: ExtensionSpec; beforeEach(() => { testSpec = { @@ -96,19 +95,19 @@ describe("provisioningHelper", () => { { apiName: "unrelated.googleapis.com", }, - ] as extensionsApi.Api[], + ] as Api[], roles: [ { role: "unrelated.role", }, - ] as extensionsApi.Role[], + ] as Role[], resources: [ { propertiesYaml: "availableMemoryMb: 1024\neventTrigger:\n eventType: providers/unrelates.service/eventTypes/something.do\n resource: projects/_/buckets/${param:IMG_BUCKET}\nlocation: ${param:LOCATION}\nruntime: nodejs10\n", }, - ] as extensionsApi.Resource[], - } as extensionsApi.ExtensionSpec; + ] as Resource[], + } as ExtensionSpec; }); it("returns empty array when nothing is used", () => { @@ -139,7 +138,7 @@ describe("provisioningHelper", () => { testSpec.resources?.push({ propertiesYaml: "availableMemoryMb: 1024\neventTrigger:\n eventType: google.storage.object.finalize\n resource: projects/_/buckets/${param:IMG_BUCKET}\nlocation: ${param:LOCATION}\nruntime: nodejs10\n", - } as extensionsApi.Resource); + } as Resource); expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ provisioningHelper.DeferredProduct.STORAGE, ]); @@ -169,7 +168,7 @@ describe("provisioningHelper", () => { testSpec.resources?.push({ propertiesYaml: "availableMemoryMb: 1024\neventTrigger:\n eventType: providers/firebase.auth/eventTypes/user.create\n resource: projects/_/buckets/${param:IMG_BUCKET}\nlocation: ${param:LOCATION}\nruntime: nodejs10\n", - } as extensionsApi.Resource); + } as Resource); expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ provisioningHelper.DeferredProduct.AUTH, ]); @@ -180,31 +179,31 @@ describe("provisioningHelper", () => { it("passes provisioning check status when nothing is used", async () => { await expect( provisioningHelper.checkProductsProvisioned(PROJECT_ID, { - resources: [] as extensionsApi.Resource[], - } as extensionsApi.ExtensionSpec) + resources: [] as Resource[], + } as ExtensionSpec), ).to.be.fulfilled; }); it("passes provisioning check when all is provisioned", async () => { - nock(api.firedataOrigin) + nock(api.firedataOrigin()) .get(`/v1/projects/${PROJECT_ID}/products`) .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); - nock(api.firebaseStorageOrigin) + nock(api.firebaseStorageOrigin()) .get(`/v1beta/projects/${PROJECT_ID}/buckets`) .reply(200, FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE); await expect( - provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH) + provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH), ).to.be.fulfilled; expect(nock.isDone()).to.be.true; }); it("fails provisioning check storage when default bucket is not linked", async () => { - nock(api.firedataOrigin) + nock(api.firedataOrigin()) .get(`/v1/projects/${PROJECT_ID}/products`) .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); - nock(api.firebaseStorageOrigin) + nock(api.firebaseStorageOrigin()) .get(`/v1beta/projects/${PROJECT_ID}/buckets`) .reply(200, { buckets: [ @@ -215,36 +214,38 @@ describe("provisioningHelper", () => { }); await expect( - provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH) + provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH), ).to.be.rejectedWith(FirebaseError, "Firebase Storage: store and retrieve user-generated"); expect(nock.isDone()).to.be.true; }); it("fails provisioning check storage when no firebase storage buckets", async () => { - nock(api.firedataOrigin) + nock(api.firedataOrigin()) .get(`/v1/projects/${PROJECT_ID}/products`) .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); - nock(api.firebaseStorageOrigin).get(`/v1beta/projects/${PROJECT_ID}/buckets`).reply(200, {}); + nock(api.firebaseStorageOrigin()) + .get(`/v1beta/projects/${PROJECT_ID}/buckets`) + .reply(200, {}); await expect( - provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH) + provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH), ).to.be.rejectedWith(FirebaseError, "Firebase Storage: store and retrieve user-generated"); expect(nock.isDone()).to.be.true; }); it("fails provisioning check storage when no auth is not provisioned", async () => { - nock(api.firedataOrigin).get(`/v1/projects/${PROJECT_ID}/products`).reply(200, {}); - nock(api.firebaseStorageOrigin) + nock(api.firedataOrigin()).get(`/v1/projects/${PROJECT_ID}/products`).reply(200, {}); + nock(api.firebaseStorageOrigin()) .get(`/v1beta/projects/${PROJECT_ID}/buckets`) .reply(200, FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE); await expect( - provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH) + provisioningHelper.checkProductsProvisioned(PROJECT_ID, SPEC_WITH_STORAGE_AND_AUTH), ).to.be.rejectedWith( FirebaseError, - "Firebase Authentication: authenticate and manage users from" + "Firebase Authentication: authenticate and manage users from", ); expect(nock.isDone()).to.be.true; @@ -253,44 +254,44 @@ describe("provisioningHelper", () => { describe("bulkCheckProductsProvisioned", () => { it("passes provisioning check status when nothing is used", async () => { - nock(api.extensionsOrigin) + nock(api.extensionsOrigin()) .get(`/v1beta/publishers/test/extensions/test/versions/0.1.0`) .reply(200, extensionVersionResponse("0.1.0", SPEC_WITH_NOTHING)); await expect( - provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]) + provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]), ).to.be.fulfilled; }); it("passes provisioning check when all is provisioned", async () => { - nock(api.extensionsOrigin) + nock(api.extensionsOrigin()) .get(`/v1beta/publishers/test/extensions/test/versions/0.1.0`) .reply(200, extensionVersionResponse("0.1.0", SPEC_WITH_STORAGE_AND_AUTH)); - nock(api.firedataOrigin) + nock(api.firedataOrigin()) .get(`/v1/projects/${PROJECT_ID}/products`) .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); - nock(api.firebaseStorageOrigin) + nock(api.firebaseStorageOrigin()) .get(`/v1beta/projects/${PROJECT_ID}/buckets`) .reply(200, FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE); await expect( - provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]) + provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]), ).to.be.fulfilled; expect(nock.isDone()).to.be.true; }); it("checks all products for multiple versions", async () => { - nock(api.extensionsOrigin) + nock(api.extensionsOrigin()) .get(`/v1beta/publishers/test/extensions/test/versions/0.1.0`) .reply(200, extensionVersionResponse("0.1.0", SPEC_WITH_STORAGE)); - nock(api.extensionsOrigin) + nock(api.extensionsOrigin()) .get(`/v1beta/publishers/test/extensions/test/versions/0.1.1`) .reply(200, extensionVersionResponse("0.1.1", SPEC_WITH_AUTH)); - nock(api.firedataOrigin) + nock(api.firedataOrigin()) .get(`/v1/projects/${PROJECT_ID}/products`) .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); - nock(api.firebaseStorageOrigin) + nock(api.firebaseStorageOrigin()) .get(`/v1beta/projects/${PROJECT_ID}/buckets`) .reply(200, FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE); @@ -298,17 +299,17 @@ describe("provisioningHelper", () => { provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [ instanceSpec("0.1.0"), instanceSpec("0.1.1"), - ]) + ]), ).to.be.fulfilled; expect(nock.isDone()).to.be.true; }); it("fails provisioning check storage when default bucket is not linked", async () => { - nock(api.extensionsOrigin) + nock(api.extensionsOrigin()) .get(`/v1beta/publishers/test/extensions/test/versions/0.1.0`) .reply(200, extensionVersionResponse("0.1.0", SPEC_WITH_STORAGE)); - nock(api.firebaseStorageOrigin) + nock(api.firebaseStorageOrigin()) .get(`/v1beta/projects/${PROJECT_ID}/buckets`) .reply(200, { buckets: [ @@ -319,23 +320,23 @@ describe("provisioningHelper", () => { }); await expect( - provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]) + provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]), ).to.be.rejectedWith(FirebaseError, "Firebase Storage: store and retrieve user-generated"); expect(nock.isDone()).to.be.true; }); it("fails provisioning check storage when no auth is not provisioned", async () => { - nock(api.extensionsOrigin) + nock(api.extensionsOrigin()) .get(`/v1beta/publishers/test/extensions/test/versions/0.1.0`) .reply(200, extensionVersionResponse("0.1.0", SPEC_WITH_AUTH)); - nock(api.firedataOrigin).get(`/v1/projects/${PROJECT_ID}/products`).reply(200, {}); + nock(api.firedataOrigin()).get(`/v1/projects/${PROJECT_ID}/products`).reply(200, {}); await expect( - provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]) + provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]), ).to.be.rejectedWith( FirebaseError, - "Firebase Authentication: authenticate and manage users from" + "Firebase Authentication: authenticate and manage users from", ); expect(nock.isDone()).to.be.true; diff --git a/src/extensions/provisioningHelper.ts b/src/extensions/provisioningHelper.ts index 6f62ff9250e..83cc85e64f4 100644 --- a/src/extensions/provisioningHelper.ts +++ b/src/extensions/provisioningHelper.ts @@ -1,11 +1,12 @@ -import * as marked from "marked"; +import { marked } from "marked"; -import * as extensionsApi from "./extensionsApi"; -import * as api from "../api"; -import * as refs from "./refs"; +import { ExtensionSpec } from "./types"; +import { firebaseStorageOrigin, firedataOrigin } from "../api"; +import { Client } from "../apiv2"; import { flattenArray } from "../functional"; import { FirebaseError } from "../error"; -import { getExtensionVersion, InstanceSpec } from "../deploy/extensions/planner"; +import { getExtensionSpec, InstanceSpec } from "../deploy/extensions/planner"; +import { logger } from "../logger"; /** Product for which provisioning can be (or is) deferred */ export enum DeferredProduct { @@ -20,7 +21,7 @@ export enum DeferredProduct { */ export async function checkProductsProvisioned( projectId: string, - spec: extensionsApi.ExtensionSpec + spec: ExtensionSpec, ): Promise { const usedProducts = getUsedProducts(spec); await checkProducts(projectId, usedProducts); @@ -33,13 +34,13 @@ export async function checkProductsProvisioned( */ export async function bulkCheckProductsProvisioned( projectId: string, - instanceSpecs: InstanceSpec[] + instanceSpecs: InstanceSpec[], ): Promise { const usedProducts = await Promise.all( instanceSpecs.map(async (i) => { - const extensionVersion = await getExtensionVersion(i); - return getUsedProducts(extensionVersion.spec); - }) + const extensionSpec = await getExtensionSpec(i); + return getUsedProducts(extensionSpec); + }), ); await checkProducts(projectId, [...flattenArray(usedProducts)]); } @@ -54,12 +55,16 @@ async function checkProducts(projectId: string, usedProducts: DeferredProduct[]) if (usedProducts.includes(DeferredProduct.AUTH)) { isAuthProvisionedPromise = isAuthProvisioned(projectId); } - - if (isStorageProvisionedPromise && !(await isStorageProvisionedPromise)) { - needProvisioning.push(DeferredProduct.STORAGE); - } - if (isAuthProvisionedPromise && !(await isAuthProvisionedPromise)) { - needProvisioning.push(DeferredProduct.AUTH); + try { + if (isStorageProvisionedPromise && !(await isStorageProvisionedPromise)) { + needProvisioning.push(DeferredProduct.STORAGE); + } + if (isAuthProvisionedPromise && !(await isAuthProvisionedPromise)) { + needProvisioning.push(DeferredProduct.AUTH); + } + } catch (err: any) { + // If a provisioning check throws, we should fail open since this is best effort. + logger.debug(`Error while checking product provisioning, failing open: ${err}`); } if (needProvisioning.length > 0) { @@ -88,7 +93,7 @@ async function checkProducts(projectId: string, usedProducts: DeferredProduct[]) * From the spec determines which products are used by the extension and * returns the list. */ -export function getUsedProducts(spec: extensionsApi.ExtensionSpec): DeferredProduct[] { +export function getUsedProducts(spec: ExtensionSpec): DeferredProduct[] { const usedProducts: DeferredProduct[] = []; const usedApis = spec.apis?.map((api) => api.apiName); const usedRoles = spec.roles?.map((r) => r.role.split(".")[0]); @@ -118,10 +123,8 @@ function getTriggerType(propertiesYaml: string | undefined) { } async function isStorageProvisioned(projectId: string): Promise { - const resp = await api.request("GET", `/v1beta/projects/${projectId}/buckets`, { - auth: true, - origin: api.firebaseStorageOrigin, - }); + const client = new Client({ urlPrefix: firebaseStorageOrigin(), apiVersion: "v1beta" }); + const resp = await client.get<{ buckets: { name: string }[] }>(`/projects/${projectId}/buckets`); return !!resp.body?.buckets?.find((bucket: any) => { const bucketResourceName = bucket.name; // Bucket resource name looks like: projects/PROJECT_NUMBER/buckets/BUCKET_NAME @@ -133,9 +136,9 @@ async function isStorageProvisioned(projectId: string): Promise { } async function isAuthProvisioned(projectId: string): Promise { - const resp = await api.request("GET", `/v1/projects/${projectId}/products`, { - auth: true, - origin: api.firedataOrigin, - }); + const client = new Client({ urlPrefix: firedataOrigin(), apiVersion: "v1" }); + const resp = await client.get<{ activation: { service: string }[] }>( + `/projects/${projectId}/products`, + ); return !!resp.body?.activation?.map((a: any) => a.service).includes("FIREBASE_AUTH"); } diff --git a/src/extensions/publishHelpers.ts b/src/extensions/publishHelpers.ts index 073f4d39229..b5d0fddabb3 100644 --- a/src/extensions/publishHelpers.ts +++ b/src/extensions/publishHelpers.ts @@ -1,5 +1,5 @@ import { consoleOrigin } from "../api"; export function consoleInstallLink(extVersionRef: string): string { - return `${consoleOrigin}/project/_/extensions/install?ref=${extVersionRef}`; + return `${consoleOrigin()}/project/_/extensions/install?ref=${extVersionRef}`; } diff --git a/src/extensions/publisherApi.spec.ts b/src/extensions/publisherApi.spec.ts new file mode 100644 index 00000000000..6d7a4f510bb --- /dev/null +++ b/src/extensions/publisherApi.spec.ts @@ -0,0 +1,616 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import * as api from "../api"; +import * as refs from "./refs"; +import * as publisherApi from "./publisherApi"; + +import { FirebaseError } from "../error"; + +const VERSION = "v1beta"; +const PROJECT_ID = "test-project"; +const PUBLISHER_ID = "test-project"; +const EXTENSION_ID = "test-extension"; +const EXTENSION_VERSION = "0.0.1"; + +const EXT_SPEC = { + name: "cool-things", + version: "1.0.0", + resources: { + name: "cool-resource", + type: "firebaseextensions.v1beta.function", + }, + sourceUrl: "www.google.com/cool-things-here", +}; +const TEST_EXTENSION_1 = { + name: "publishers/test-pub/extensions/ext-one", + ref: "test-pub/ext-one", + state: "PUBLISHED", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXTENSION_2 = { + name: "publishers/test-pub/extensions/ext-two", + ref: "test-pub/ext-two", + state: "PUBLISHED", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXTENSION_3 = { + name: "publishers/test-pub/extensions/ext-three", + ref: "test-pub/ext-three", + state: "UNPUBLISHED", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXT_VERSION_1 = { + name: "publishers/test-pub/extensions/ext-one/versions/0.0.1", + ref: "test-pub/ext-one@0.0.1", + spec: EXT_SPEC, + state: "UNPUBLISHED", + hash: "12345", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXT_VERSION_2 = { + name: "publishers/test-pub/extensions/ext-one/versions/0.0.2", + ref: "test-pub/ext-one@0.0.2", + spec: EXT_SPEC, + state: "PUBLISHED", + hash: "23456", + createTime: "2020-06-30T00:21:06.722782Z", +}; +const TEST_EXT_VERSION_3 = { + name: "publishers/test-pub/extensions/ext-one/versions/0.0.3", + ref: "test-pub/ext-one@0.0.3", + spec: EXT_SPEC, + state: "PUBLISHED", + hash: "34567", + createTime: "2020-06-30T00:21:06.722782Z", +}; + +const TEST_EXT_VERSION_4 = { + name: "publishers/test-pub/extensions/ext-one/versions/0.0.4", + ref: "test-pub/ext-one@0.0.4", + spec: EXT_SPEC, + state: "DEPRECATED", + hash: "34567", + createTime: "2020-06-30T00:21:06.722782Z", + deprecationMessage: "This version is deprecated", +}; + +const NEXT_PAGE_TOKEN = "random123"; +const PUBLISHED_EXTENSIONS = { extensions: [TEST_EXTENSION_1, TEST_EXTENSION_2] }; +const ALL_EXTENSIONS = { + extensions: [TEST_EXTENSION_1, TEST_EXTENSION_2, TEST_EXTENSION_3], +}; +const PUBLISHED_WITH_TOKEN = { extensions: [TEST_EXTENSION_1], nextPageToken: NEXT_PAGE_TOKEN }; +const NEXT_PAGE_EXTENSIONS = { extensions: [TEST_EXTENSION_2] }; + +const PUBLISHED_EXT_VERSIONS = { extensionVersions: [TEST_EXT_VERSION_2, TEST_EXT_VERSION_3] }; +const ALL_EXT_VERSIONS = { + extensionVersions: [TEST_EXT_VERSION_1, TEST_EXT_VERSION_2, TEST_EXT_VERSION_3], +}; +const PUBLISHED_VERSIONS_WITH_TOKEN = { + extensionVersions: [TEST_EXT_VERSION_2], + nextPageToken: NEXT_PAGE_TOKEN, +}; +const NEXT_PAGE_VERSIONS = { extensionVersions: [TEST_EXT_VERSION_3] }; + +describe("createExtensionVersionFromGitHubSource", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a POST call to the correct endpoint, and then poll on the returned operation", async () => { + nock(api.extensionsPublisherOrigin()) + .post(`/${VERSION}/publishers/test-pub/extensions/ext-one/versions:createFromSource`) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsPublisherOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { + done: true, + response: TEST_EXT_VERSION_3, + }); + + const res = await publisherApi.createExtensionVersionFromGitHubSource({ + extensionVersionRef: TEST_EXT_VERSION_3.ref, + repoUri: "https://github.com/username/repo", + sourceRef: "HEAD", + extensionRoot: "/", + }); + expect(res).to.deep.equal(TEST_EXT_VERSION_3); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if createExtensionVersionFromLocalSource returns an error response", async () => { + nock(api.extensionsPublisherOrigin()) + .post( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions:createFromSource`, + ) + .reply(500); + + await expect( + publisherApi.createExtensionVersionFromGitHubSource({ + extensionVersionRef: `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, + repoUri: "https://github.com/username/repo", + sourceRef: "HEAD", + extensionRoot: "/", + }), + ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500, Unknown Error"); + expect(nock.isDone()).to.be.true; + }); + + it("stop polling and throw if the operation call throws an unexpected error", async () => { + nock(api.extensionsPublisherOrigin()) + .post( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions:createFromSource`, + ) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsPublisherOrigin()).get(`/${VERSION}/operations/abc123`).reply(502, {}); + + await expect( + publisherApi.createExtensionVersionFromGitHubSource({ + extensionVersionRef: `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, + repoUri: "https://github.com/username/repo", + sourceRef: "HEAD", + extensionRoot: "/", + }), + ).to.be.rejectedWith(FirebaseError, "HTTP Error: 502, Unknown Error"); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect( + publisherApi.createExtensionVersionFromGitHubSource({ + extensionVersionRef: `${PUBLISHER_ID}/${EXTENSION_ID}`, + repoUri: "https://github.com/username/repo", + sourceRef: "HEAD", + extensionRoot: "/", + }), + ).to.be.rejectedWith(FirebaseError, "Extension version ref"); + }); +}); + +describe("createExtensionVersionFromLocalSource", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a POST call to the correct endpoint, and then poll on the returned operation", async () => { + nock(api.extensionsPublisherOrigin()) + .post(`/${VERSION}/publishers/test-pub/extensions/ext-one/versions:createFromSource`) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsPublisherOrigin()).get(`/${VERSION}/operations/abc123`).reply(200, { + done: true, + response: TEST_EXT_VERSION_3, + }); + + const res = await publisherApi.createExtensionVersionFromLocalSource({ + extensionVersionRef: TEST_EXT_VERSION_3.ref, + packageUri: "www.google.com/test-extension.zip", + }); + expect(res).to.deep.equal(TEST_EXT_VERSION_3); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if createExtensionVersionFromLocalSource returns an error response", async () => { + nock(api.extensionsPublisherOrigin()) + .post( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions:createFromSource`, + ) + .reply(500); + + await expect( + publisherApi.createExtensionVersionFromLocalSource({ + extensionVersionRef: `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, + packageUri: "www.google.com/test-extension.zip", + extensionRoot: "/", + }), + ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500, Unknown Error"); + expect(nock.isDone()).to.be.true; + }); + + it("stop polling and throw if the operation call throws an unexpected error", async () => { + nock(api.extensionsPublisherOrigin()) + .post( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions:createFromSource`, + ) + .reply(200, { name: "operations/abc123" }); + nock(api.extensionsPublisherOrigin()).get(`/${VERSION}/operations/abc123`).reply(502, {}); + + await expect( + publisherApi.createExtensionVersionFromLocalSource({ + extensionVersionRef: `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, + packageUri: "www.google.com/test-extension.zip", + extensionRoot: "/", + }), + ).to.be.rejectedWith(FirebaseError, "HTTP Error: 502, Unknown Error"); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect( + publisherApi.createExtensionVersionFromLocalSource({ + extensionVersionRef: `${PUBLISHER_ID}/${EXTENSION_ID}`, + packageUri: "www.google.com/test-extension.zip", + extensionRoot: "/", + }), + ).to.be.rejectedWith(FirebaseError, "Extension version ref"); + }); +}); + +describe("getExtension", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a GET call to the correct endpoint", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) + .reply(200); + + await publisherApi.getExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) + .reply(404); + + await expect(publisherApi.getExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`)).to.be.rejectedWith( + FirebaseError, + ); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect(publisherApi.getExtension(`${PUBLISHER_ID}`)).to.be.rejectedWith( + FirebaseError, + "Unable to parse", + ); + }); +}); + +describe("getExtensionVersion", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a GET call to the correct endpoint", async () => { + nock(api.extensionsPublisherOrigin()) + .get( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions/${EXTENSION_VERSION}`, + ) + .reply(200, TEST_EXTENSION_1); + + const got = await publisherApi.getExtensionVersion( + `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, + ); + expect(got).to.deep.equal(TEST_EXTENSION_1); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsPublisherOrigin()) + .get( + `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions/${EXTENSION_VERSION}`, + ) + .reply(404); + + await expect( + publisherApi.getExtensionVersion(`${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`), + ).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect( + publisherApi.getExtensionVersion(`${PUBLISHER_ID}//${EXTENSION_ID}`), + ).to.be.rejectedWith(FirebaseError, "Unable to parse"); + }); +}); + +describe("listExtensions", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should return a list of published extensions", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(200, PUBLISHED_EXTENSIONS); + + const extensions = await publisherApi.listExtensions(PUBLISHER_ID); + expect(extensions).to.deep.equal(PUBLISHED_EXTENSIONS.extensions); + expect(nock.isDone()).to.be.true; + }); + + it("should return a list of all extensions", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(200, ALL_EXTENSIONS); + + const extensions = await publisherApi.listExtensions(PUBLISHER_ID); + + expect(extensions).to.deep.equal(ALL_EXTENSIONS.extensions); + expect(nock.isDone()).to.be.true; + }); + + it("should query for more extensions if the response has a next_page_token", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(200, PUBLISHED_WITH_TOKEN); + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + queryParams.pageToken === NEXT_PAGE_TOKEN; + return queryParams; + }) + .reply(200, NEXT_PAGE_EXTENSIONS); + + const extensions = await publisherApi.listExtensions(PUBLISHER_ID); + + const expected = PUBLISHED_WITH_TOKEN.extensions.concat(NEXT_PAGE_EXTENSIONS.extensions); + expect(extensions).to.deep.equal(expected); + expect(nock.isDone()).to.be.true; + }); + + it("should throw FirebaseError if any call returns an error", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) + .query((queryParams: any) => { + queryParams.pageSize === "100"; + return queryParams; + }) + .reply(503, PUBLISHED_EXTENSIONS); + + await expect(publisherApi.listExtensions(PUBLISHER_ID)).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); +}); + +describe("listExtensionVersions", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should return a list of published extension versions", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, PUBLISHED_EXT_VERSIONS); + + const extensions = await publisherApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); + expect(extensions).to.deep.equal(PUBLISHED_EXT_VERSIONS.extensionVersions); + expect(nock.isDone()).to.be.true; + }); + + it("should send filter query param", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100" && queryParams.filter === "id<1.0.0"; + }) + .reply(200, PUBLISHED_EXT_VERSIONS); + + const extensions = await publisherApi.listExtensionVersions( + `${PUBLISHER_ID}/${EXTENSION_ID}`, + "id<1.0.0", + ); + expect(extensions).to.deep.equal(PUBLISHED_EXT_VERSIONS.extensionVersions); + expect(nock.isDone()).to.be.true; + }); + + it("should return a list of all extension versions", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, ALL_EXT_VERSIONS); + + const extensions = await publisherApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); + + expect(extensions).to.deep.equal(ALL_EXT_VERSIONS.extensionVersions); + expect(nock.isDone()).to.be.true; + }); + + it("should query for more extension versions if the response has a next_page_token", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100" && queryParams.pageToken === NEXT_PAGE_TOKEN; + }) + .reply(200, NEXT_PAGE_VERSIONS); + + const extensions = await publisherApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); + + const expected = PUBLISHED_VERSIONS_WITH_TOKEN.extensionVersions.concat( + NEXT_PAGE_VERSIONS.extensionVersions, + ); + expect(extensions).to.deep.equal(expected); + expect(nock.isDone()).to.be.true; + }); + + it("should throw FirebaseError if any call returns an error", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100"; + }) + .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) + .query((queryParams: any) => { + return queryParams.pageSize === "100" && queryParams.pageToken === NEXT_PAGE_TOKEN; + }) + .reply(500); + + await expect( + publisherApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`), + ).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error for an invalid ref", async () => { + await expect(publisherApi.listExtensionVersions("")).to.be.rejectedWith( + FirebaseError, + "Unable to parse", + ); + }); +}); + +describe("getPublisherProfile", () => { + afterEach(() => { + nock.cleanAll(); + }); + + const PUBLISHER_PROFILE = { + name: "projects/test-publisher/publisherProfile", + publisherId: "test-publisher", + registerTime: "2020-06-30T00:21:06.722782Z", + }; + it("should make a GET call to the correct endpoint", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/publisherProfile`) + .query(true) + .reply(200, PUBLISHER_PROFILE); + + const res = await publisherApi.getPublisherProfile(PROJECT_ID); + expect(res).to.deep.equal(PUBLISHER_PROFILE); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsPublisherOrigin()) + .get(`/${VERSION}/projects/${PROJECT_ID}/publisherProfile`) + .query(true) + .reply(404); + + await expect(publisherApi.getPublisherProfile(PROJECT_ID)).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); +}); + +describe("registerPublisherProfile", () => { + afterEach(() => { + nock.cleanAll(); + }); + + const PUBLISHER_PROFILE = { + name: "projects/test-publisher/publisherProfile", + publisherId: "test-publisher", + registerTime: "2020-06-30T00:21:06.722782Z", + }; + it("should make a POST call to the correct endpoint", async () => { + nock(api.extensionsPublisherOrigin()) + .patch( + `/${VERSION}/projects/${PROJECT_ID}/publisherProfile?updateMask=publisher_id%2Cdisplay_name`, + ) + .reply(200, PUBLISHER_PROFILE); + + const res = await publisherApi.registerPublisherProfile(PROJECT_ID, PUBLISHER_ID); + expect(res).to.deep.equal(PUBLISHER_PROFILE); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.extensionsPublisherOrigin()) + .patch( + `/${VERSION}/projects/${PROJECT_ID}/publisherProfile?updateMask=publisher_id%2Cdisplay_name`, + ) + .reply(404); + await expect( + publisherApi.registerPublisherProfile(PROJECT_ID, PUBLISHER_ID), + ).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); +}); + +describe("deprecateExtensionVersion", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a POST call to the correct endpoint", async () => { + const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_4.ref); + nock(api.extensionsPublisherOrigin()) + .persist() + .post( + `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:deprecate`, + ) + .reply(200, TEST_EXT_VERSION_4); + + const res = await publisherApi.deprecateExtensionVersion( + TEST_EXT_VERSION_4.ref, + "This version is deprecated.", + ); + expect(res).to.deep.equal(TEST_EXT_VERSION_4); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_4.ref); + nock(api.extensionsPublisherOrigin()) + .persist() + .post( + `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:deprecate`, + ) + .reply(404); + await expect( + publisherApi.deprecateExtensionVersion(TEST_EXT_VERSION_4.ref, "This version is deprecated."), + ).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); +}); + +describe("undeprecateExtensionVersion", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should make a POST call to the correct endpoint", async () => { + const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_3.ref); + nock(api.extensionsPublisherOrigin()) + .persist() + .post( + `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:undeprecate`, + ) + .reply(200, TEST_EXT_VERSION_3); + + const res = await publisherApi.undeprecateExtensionVersion(TEST_EXT_VERSION_3.ref); + expect(res).to.deep.equal(TEST_EXT_VERSION_3); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_3.ref); + nock(api.extensionsPublisherOrigin()) + .persist() + .post( + `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:undeprecate`, + ) + .reply(404); + await expect( + publisherApi.undeprecateExtensionVersion(TEST_EXT_VERSION_3.ref), + ).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); +}); diff --git a/src/extensions/publisherApi.ts b/src/extensions/publisherApi.ts new file mode 100644 index 00000000000..df114149049 --- /dev/null +++ b/src/extensions/publisherApi.ts @@ -0,0 +1,329 @@ +import * as clc from "colorette"; + +import * as operationPoller from "../operation-poller"; +import * as refs from "./refs"; + +import { extensionsPublisherOrigin } from "../api"; +import { Client } from "../apiv2"; +import { FirebaseError } from "../error"; +import { populateSpec, refNotFoundError } from "./extensionsApi"; +import { Extension, ExtensionVersion, PublisherProfile } from "./types"; + +const PUBLISHER_API_VERSION = "v1beta"; +const PAGE_SIZE_MAX = 100; + +const extensionsPublisherApiClient = new Client({ + urlPrefix: extensionsPublisherOrigin(), + apiVersion: PUBLISHER_API_VERSION, +}); + +/** + * @param projectId the project for which we are registering a PublisherProfile + * @param publisherId the desired publisher ID + */ +export async function getPublisherProfile( + projectId: string, + publisherId?: string, +): Promise { + const res = await extensionsPublisherApiClient.get(`/projects/${projectId}/publisherProfile`, { + queryParams: + publisherId === undefined + ? undefined + : { + publisherId, + }, + }); + return res.body as PublisherProfile; +} + +/** + * @param projectId the project for which we are registering a PublisherProfile + * @param publisherId the desired publisher ID + */ +export async function registerPublisherProfile( + projectId: string, + publisherId: string, +): Promise { + const res = await extensionsPublisherApiClient.patch, PublisherProfile>( + `/projects/${projectId}/publisherProfile`, + { + publisherId, + displayName: publisherId, + }, + { + queryParams: { + updateMask: "publisher_id,display_name", + }, + }, + ); + return res.body; +} + +/** + * @param extensionRef user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@version) + * @param deprecationMessage the deprecation message + */ +export async function deprecateExtensionVersion( + extensionRef: string, + deprecationMessage: string, +): Promise { + const ref = refs.parse(extensionRef); + try { + const res = await extensionsPublisherApiClient.post< + { deprecationMessage: string }, + ExtensionVersion + >(`/${refs.toExtensionVersionName(ref)}:deprecate`, { + deprecationMessage, + }); + return res.body; + } catch (err: any) { + if (err.status === 403) { + throw new FirebaseError( + `You are not the owner of extension '${clc.bold( + extensionRef, + )}' and don’t have the correct permissions to deprecate this extension version.` + err, + { status: err.status }, + ); + } else if (err.status === 404) { + throw new FirebaseError(`Extension version ${clc.bold(extensionRef)} was not found.`); + } else if (err instanceof FirebaseError) { + throw err; + } + throw new FirebaseError( + `Error occurred deprecating extension version '${extensionRef}': ${err}`, + { + status: err.status, + }, + ); + } +} + +/** + * @param extensionRef user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@version) + */ +export async function undeprecateExtensionVersion(extensionRef: string): Promise { + const ref = refs.parse(extensionRef); + try { + const res = await extensionsPublisherApiClient.post( + `/${refs.toExtensionVersionName(ref)}:undeprecate`, + ); + return res.body; + } catch (err: any) { + if (err.status === 403) { + throw new FirebaseError( + `You are not the owner of extension '${clc.bold( + extensionRef, + )}' and don’t have the correct permissions to undeprecate this extension version.`, + { status: err.status }, + ); + } else if (err.status === 404) { + throw new FirebaseError(`Extension version ${clc.bold(extensionRef)} was not found.`); + } else if (err instanceof FirebaseError) { + throw err; + } + throw new FirebaseError( + `Error occurred undeprecating extension version '${extensionRef}': ${err}`, + { + status: err.status, + }, + ); + } +} + +/** + * @param extensionVersionRef user-friendly identifier for the extension version (publisher-id/extension-id@1.0.0) + * @param packageUri public URI of the extension archive (zip or tarball) + * @param extensionRoot root directory that contains this extension, defaults to "/". + */ +export async function createExtensionVersionFromLocalSource(args: { + extensionVersionRef: string; + packageUri: string; + extensionRoot?: string; +}): Promise { + const ref = refs.parse(args.extensionVersionRef); + if (!ref.version) { + throw new FirebaseError( + `Extension version ref "${args.extensionVersionRef}" must supply a version.`, + ); + } + // TODO(b/185176470): Publishing an extension with a previously deleted name will return 409. + // Need to surface a better error, potentially by calling getExtension. + const uploadRes = await extensionsPublisherApiClient.post< + { + versionId: string; + extensionRoot: string; + remoteArchiveSource: { + packageUri: string; + }; + }, + ExtensionVersion + >(`/${refs.toExtensionName(ref)}/versions:createFromSource`, { + versionId: ref.version, + extensionRoot: args.extensionRoot ?? "/", + remoteArchiveSource: { + packageUri: args.packageUri, + }, + }); + const pollRes = await operationPoller.pollOperation({ + apiOrigin: extensionsPublisherOrigin(), + apiVersion: PUBLISHER_API_VERSION, + operationResourceName: uploadRes.body.name, + masterTimeout: 600000, + }); + return pollRes; +} + +/** + * @param extensionVersionRef user-friendly identifier for the extension version (publisher-id/extension-id@1.0.0) + * @param repoUri public GitHub repo URI that contains the extension source + * @param sourceRef commit hash, branch, or tag to build from the repo + * @param extensionRoot root directory that contains this extension, defaults to "/". + */ +export async function createExtensionVersionFromGitHubSource(args: { + extensionVersionRef: string; + repoUri: string; + sourceRef: string; + extensionRoot?: string; +}): Promise { + const ref = refs.parse(args.extensionVersionRef); + if (!ref.version) { + throw new FirebaseError( + `Extension version ref "${args.extensionVersionRef}" must supply a version.`, + ); + } + // TODO(b/185176470): Publishing an extension with a previously deleted name will return 409. + // Need to surface a better error, potentially by calling getExtension. + const uploadRes = await extensionsPublisherApiClient.post< + { + versionId: string; + extensionRoot: string; + githubRepositorySource: { + uri: string; + sourceRef: string; + }; + }, + ExtensionVersion + >(`/${refs.toExtensionName(ref)}/versions:createFromSource`, { + versionId: ref.version, + extensionRoot: args.extensionRoot || "/", + githubRepositorySource: { + uri: args.repoUri, + sourceRef: args.sourceRef, + }, + }); + const pollRes = await operationPoller.pollOperation({ + apiOrigin: extensionsPublisherOrigin(), + apiVersion: PUBLISHER_API_VERSION, + operationResourceName: uploadRes.body.name, + masterTimeout: 600000, + }); + return pollRes; +} + +/** + * @param ref user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@1.0.0) + */ +export async function getExtensionVersion(extensionVersionRef: string): Promise { + const ref = refs.parse(extensionVersionRef); + if (!ref.version) { + throw new FirebaseError(`ExtensionVersion ref "${extensionVersionRef}" must supply a version.`); + } + try { + const res = await extensionsPublisherApiClient.get( + `/${refs.toExtensionVersionName(ref)}`, + ); + if (res.body.spec) { + populateSpec(res.body.spec); + } + return res.body; + } catch (err: any) { + if (err.status === 404) { + throw refNotFoundError(ref); + } else if (err instanceof FirebaseError) { + throw err; + } + throw new FirebaseError( + `Failed to query the extension version '${clc.bold(extensionVersionRef)}': ${err}`, + ); + } +} + +/** + * @param publisherId the publisher for which we are listing Extensions + */ +export async function listExtensions(publisherId: string): Promise { + const extensions: Extension[] = []; + const getNextPage = async (pageToken = "") => { + const res = await extensionsPublisherApiClient.get<{ + extensions: Extension[]; + nextPageToken: string; + }>(`/publishers/${publisherId}/extensions`, { + queryParams: { + pageSize: PAGE_SIZE_MAX, + pageToken, + }, + }); + if (Array.isArray(res.body.extensions)) { + extensions.push(...res.body.extensions); + } + if (res.body.nextPageToken) { + await getNextPage(res.body.nextPageToken); + } + }; + await getNextPage(); + return extensions; +} + +/** + * @param ref user-friendly identifier for the ExtensionVersion (publisher-id/extension-id) + */ +export async function listExtensionVersions( + ref: string, + filter = "", + showPrereleases = false, +): Promise { + const { publisherId, extensionId } = refs.parse(ref); + const extensionVersions: ExtensionVersion[] = []; + const getNextPage = async (pageToken = "") => { + const res = await extensionsPublisherApiClient.get<{ + extensionVersions: ExtensionVersion[]; + nextPageToken: string; + }>(`/publishers/${publisherId}/extensions/${extensionId}/versions`, { + queryParams: { + filter, + showPrereleases: String(showPrereleases), + pageSize: PAGE_SIZE_MAX, + pageToken, + }, + }); + if (Array.isArray(res.body.extensionVersions)) { + extensionVersions.push(...res.body.extensionVersions); + } + if (res.body.nextPageToken) { + await getNextPage(res.body.nextPageToken); + } + }; + await getNextPage(); + return extensionVersions; +} + +/** + * @param ref user-friendly identifier for the Extension (publisher-id/extension-id) + * @return the extension + */ +export async function getExtension(extensionRef: string): Promise { + const ref = refs.parse(extensionRef); + try { + const res = await extensionsPublisherApiClient.get(`/${refs.toExtensionName(ref)}`); + return res.body; + } catch (err: any) { + if (err.status === 404) { + throw refNotFoundError(ref); + } else if (err instanceof FirebaseError) { + throw err; + } + throw new FirebaseError(`Failed to query the extension '${clc.bold(extensionRef)}': ${err}`, { + status: err.status, + }); + } +} diff --git a/src/extensions/refs.ts b/src/extensions/refs.ts index 232da321d1f..12c50392410 100644 --- a/src/extensions/refs.ts +++ b/src/extensions/refs.ts @@ -28,10 +28,10 @@ export function parse(refOrName: string): Ref { ret.version && !semver.valid(ret.version) && !semver.validRange(ret.version) && - ret.version !== "latest" + !["latest", "latest-approved"].includes(ret.version) ) { throw new FirebaseError( - `Extension reference ${ret} contains an invalid version ${ret.version}.` + `Extension reference ${ret} contains an invalid version ${ret.version}.`, ); } return ret; @@ -40,7 +40,7 @@ export function parse(refOrName: string): Ref { function parseRef(ref: string): Ref | undefined { const parts = refRegex.exec(ref); // Exec additionally returns original string, index, & input values. - if (parts && (parts.length == 5 || parts.length == 7)) { + if (parts && (parts.length === 5 || parts.length === 7)) { const publisherId = parts[1]; const extensionId = parts[2]; const version = parts[4]; @@ -65,7 +65,7 @@ export function toExtensionRef(ref: Ref): string { } /** - * To an extension bersion ref: publisherId/extensionId@version + * To an extension version ref: publisherId/extensionId@version */ export function toExtensionVersionRef(ref: Ref): string { if (!ref.version) { diff --git a/src/extensions/resolveSource.ts b/src/extensions/resolveSource.ts index f5f6db1137f..3a06fc4df2e 100644 --- a/src/extensions/resolveSource.ts +++ b/src/extensions/resolveSource.ts @@ -1,107 +1,13 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as marked from "marked"; -import * as semver from "semver"; -import * as api from "../api"; -import { FirebaseError } from "../error"; -import { logger } from "../logger"; -import { promptOnce } from "../prompt"; +import { Client } from "../apiv2"; +import { firebaseExtensionsRegistryOrigin } from "../api"; const EXTENSIONS_REGISTRY_ENDPOINT = "/extensions.json"; -export interface RegistryEntry { - icons?: { [key: string]: string }; - labels: { [key: string]: string }; - versions: { [key: string]: string }; - updateWarnings?: { [key: string]: UpdateWarning[] }; - publisher: string; -} - -export interface UpdateWarning { - from: string; - description: string; - action?: string; -} - /** - * Displays an update warning as markdown, and prompts the user for confirmation. - * @param updateWarning The update warning to display and prompt for. + * An Entry on the deprecated registry.json list. */ -export async function confirmUpdateWarning(updateWarning: UpdateWarning): Promise { - logger.info(marked(updateWarning.description)); - if (updateWarning.action) { - logger.info(marked(updateWarning.action)); - } - const continueUpdate = await promptOnce({ - type: "confirm", - message: "Do you wish to continue with this update?", - default: false, - }); - if (!continueUpdate) { - throw new FirebaseError(`Update cancelled.`, { exit: 2 }); - } -} - -/** - * Gets the sourceUrl for a given extension name and version from a registry entry - * @param registryEntry the registry entry to look through. - * @param name the name of the extension. - * @param version the version of the extension. Defaults to latest. - * @returns the source corresponding to extensionName in the registry. - */ -export function resolveSourceUrl( - registryEntry: RegistryEntry, - name: string, - version?: string -): string { - const targetVersion = getTargetVersion(registryEntry, version); - const sourceUrl = _.get(registryEntry, ["versions", targetVersion]); - if (!sourceUrl) { - throw new FirebaseError( - `Could not find version ${clc.bold(version)} of extension ${clc.bold(name)}.` - ); - } - return sourceUrl; -} - -/** - * Checks if the given source comes from an official extension. - * @param registryEntry the registry entry to look through. - * @param sourceUrl the source URL of the extension. - */ -export function isOfficialSource(registryEntry: RegistryEntry, sourceUrl: string): boolean { - const versions = _.get(registryEntry, "versions"); - return _.includes(versions, sourceUrl); -} - -/** - * Looks up and returns a entry from the published extensions registry. - * @param name the name of the extension. - */ -export async function resolveRegistryEntry(name: string): Promise { - const extensionsRegistry = await getExtensionRegistry(); - const registryEntry = _.get(extensionsRegistry, name); - if (!registryEntry) { - throw new FirebaseError(`Unable to find extension source named ${clc.bold(name)}.`); - } - return registryEntry; -} - -/** - * Resolves a version or label to a version. - * @param registryEntry A registry entry to get the version from. - * @param versionOrLabel A version or label to resolve. Defaults to 'latest'. - */ -export function getTargetVersion(registryEntry: RegistryEntry, versionOrLabel?: string): string { - // The version to search for when a user passes a version x.y.z or no version. - const seekVersion = versionOrLabel || "latest"; - // The version to search for when a user passes a label like 'latest'. - const versionFromLabel = _.get(registryEntry, ["labels", seekVersion]); - return versionFromLabel || seekVersion; -} - -export function getMinRequiredVersion(registryEntry: RegistryEntry): string { - return _.get(registryEntry, ["labels", "minRequired"]); +export interface RegistryEntry { + publisher: string; } /** @@ -109,40 +15,24 @@ export function getMinRequiredVersion(registryEntry: RegistryEntry): string { * @param onlyFeatured If true, only return the featured extensions. */ export async function getExtensionRegistry( - onlyFeatured?: boolean -): Promise<{ [key: string]: RegistryEntry }> { - const res = await api.request("GET", EXTENSIONS_REGISTRY_ENDPOINT, { - origin: api.firebaseExtensionsRegistryOrigin, - }); - const extensions = _.get(res, "body.mods") as { [key: string]: RegistryEntry }; + onlyFeatured = false, +): Promise> { + const client = new Client({ urlPrefix: firebaseExtensionsRegistryOrigin() }); + const res = await client.get<{ + mods?: Record; + featured?: { discover?: string[] }; + }>(EXTENSIONS_REGISTRY_ENDPOINT); + const extensions: Record = res.body.mods || {}; if (onlyFeatured) { - const featuredList = _.get(res, "body.featured.discover"); - return _.pickBy(extensions, (_entry, extensionName: string) => { - return _.includes(featuredList, extensionName); - }); + const featuredList = new Set(res.body.featured?.discover || []); + const filteredExtensions: Record = {}; + for (const [name, extension] of Object.entries(extensions)) { + if (featuredList.has(name)) { + filteredExtensions[name] = extension; + } + } + return filteredExtensions; } return extensions; } - -/** - * Fetches a list all publishers that appear in the v1 registry. - */ -export async function getTrustedPublishers(): Promise { - let registry: { [key: string]: RegistryEntry }; - try { - registry = await getExtensionRegistry(); - } catch (err) { - logger.debug( - "Couldn't get extensions registry, assuming no trusted publishers except Firebase." - ); - return ["firebase"]; - } - const publisherIds = new Set(); - - // eslint-disable-next-line guard-for-in - for (const entry in registry) { - publisherIds.add(registry[entry].publisher); - } - return Array.from(publisherIds); -} diff --git a/src/extensions/secretUtils.spec.ts b/src/extensions/secretUtils.spec.ts new file mode 100644 index 00000000000..72bb7629448 --- /dev/null +++ b/src/extensions/secretUtils.spec.ts @@ -0,0 +1,83 @@ +import * as nock from "nock"; +import { expect } from "chai"; + +import * as api from "../api"; +import { ExtensionInstance, ParamType } from "./types"; +import * as secretsUtils from "./secretsUtils"; + +const PROJECT_ID = "test-project"; +const TEST_INSTANCE: ExtensionInstance = { + name: "projects/invader-zim/instances/image-resizer", + createTime: "2019-05-19T00:20:10.416947Z", + updateTime: "2019-05-19T00:20:10.416947Z", + state: "ACTIVE", + serviceAccountEmail: "service@account.com", + config: { + name: "projects/invader-zim/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + source: { + name: "", + state: "ACTIVE", + packageUri: "url", + hash: "hash", + spec: { + name: "test", + displayName: "Old", + description: "descriptive", + version: "1.0.0", + license: "MIT", + resources: [], + author: { authorName: "Tester" }, + contributors: [{ authorName: "Tester 2" }], + billingRequired: true, + sourceUrl: "test.com", + params: [ + { + param: "SECRET1", + label: "secret 1", + type: ParamType.SECRET, + }, + { + param: "SECRET2", + label: "secret 2", + type: ParamType.SECRET, + }, + ], + systemParams: [], + }, + }, + params: { + SECRET1: "projects/test-project/secrets/secret1/versions/1", + SECRET2: "projects/test-project/secrets/secret2/versions/1", + }, + systemParams: {}, + }, +}; + +describe("secretsUtils", () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe("getManagedSecrets", () => { + it("only returns secrets that have labels set", async () => { + nock(api.secretManagerOrigin()) + .get(`/v1/projects/${PROJECT_ID}/secrets/secret1`) + .reply(200, { + name: `projects/${PROJECT_ID}/secrets/secret1`, + labels: { "firebase-extensions-managed": "true" }, + }); + nock(api.secretManagerOrigin()) + .get(`/v1/projects/${PROJECT_ID}/secrets/secret2`) + .reply(200, { + name: `projects/${PROJECT_ID}/secrets/secret2`, + }); // no labels + + expect(await secretsUtils.getManagedSecrets(TEST_INSTANCE)).to.deep.equal([ + "projects/test-project/secrets/secret1/versions/1", + ]); + + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/extensions/secretsUtils.ts b/src/extensions/secretsUtils.ts index 2aee6959c82..0cbfcc75dc1 100644 --- a/src/extensions/secretsUtils.ts +++ b/src/extensions/secretsUtils.ts @@ -2,37 +2,37 @@ import { getProjectNumber } from "../getProjectNumber"; import * as utils from "../utils"; import { ensure } from "../ensureApiEnabled"; import { needProjectId } from "../projectUtils"; -import * as extensionsApi from "./extensionsApi"; +import { ExtensionInstance, ExtensionSpec, ParamType } from "./types"; import * as secretManagerApi from "../gcp/secretManager"; import { logger } from "../logger"; +import { secretManagerOrigin } from "../api"; export const SECRET_LABEL = "firebase-extensions-managed"; +export const SECRET_ROLE = "secretmanager.secretAccessor"; export async function ensureSecretManagerApiEnabled(options: any): Promise { const projectId = needProjectId(options); - return await ensure(projectId, "secretmanager.googleapis.com", "extensions", options.markdown); + return await ensure(projectId, secretManagerOrigin(), "extensions", options.markdown); } -export function usesSecrets(spec: extensionsApi.ExtensionSpec): boolean { - return spec.params && !!spec.params.find((p) => p.type == extensionsApi.ParamType.SECRET); +export function usesSecrets(spec: ExtensionSpec): boolean { + return spec.params && !!spec.params.find((p) => p.type === ParamType.SECRET); } export async function grantFirexServiceAgentSecretAdminRole( - secret: secretManagerApi.Secret + secret: secretManagerApi.Secret, ): Promise { const projectNumber = await getProjectNumber({ projectId: secret.projectId }); const firexSaProjectId = utils.envOverride( "FIREBASE_EXTENSIONS_SA_PROJECT_ID", - "gcp-sa-firebasemods" + "gcp-sa-firebasemods", ); const saEmail = `service-${projectNumber}@${firexSaProjectId}.iam.gserviceaccount.com`; - return secretManagerApi.grantServiceAgentRole(secret, saEmail, "roles/secretmanager.admin"); + return secretManagerApi.ensureServiceAgentRole(secret, [saEmail], "roles/secretmanager.admin"); } -export async function getManagedSecrets( - instance: extensionsApi.ExtensionInstance -): Promise { +export async function getManagedSecrets(instance: ExtensionInstance): Promise { return ( await Promise.all( getActiveSecrets(instance.config.source.spec, instance.config.params).map( @@ -43,18 +43,15 @@ export async function getManagedSecrets( return secretResourceName; } return Promise.resolve(""); - } - ) + }, + ), ) ).filter((secretId) => !!secretId); } -export function getActiveSecrets( - spec: extensionsApi.ExtensionSpec, - params: Record -): string[] { +export function getActiveSecrets(spec: ExtensionSpec, params: Record): string[] { return spec.params - .map((p) => (p.type == extensionsApi.ParamType.SECRET ? params[p.param] : "")) + .map((p) => (p.type === ParamType.SECRET ? params[p.param] : "")) .filter((pv) => !!pv); } @@ -66,7 +63,7 @@ export function getSecretLabels(instanceId: string): Record { export function prettySecretName(secretResourceName: string): string { const nameTokens = secretResourceName.split("/"); - if (nameTokens.length != 4 && nameTokens.length != 6) { + if (nameTokens.length !== 4 && nameTokens.length !== 6) { // not a familiar format, return as is logger.debug(`unable to parse secret secretResourceName: ${secretResourceName}`); return secretResourceName; diff --git a/src/extensions/tos.spec.ts b/src/extensions/tos.spec.ts new file mode 100644 index 00000000000..72c630380d3 --- /dev/null +++ b/src/extensions/tos.spec.ts @@ -0,0 +1,194 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import * as api from "../api"; +import * as tos from "./tos"; + +describe("tos", () => { + afterEach(() => { + nock.cleanAll(); + }); + + const testProjectId = "test-proj"; + describe("getAppDeveloperTOSStatus", () => { + it("should get app developer TOS", async () => { + const t = testTOS("appdevtos", "1.0.0"); + nock(api.extensionsTOSOrigin()).get(`/v1/projects/${testProjectId}/appdevtos`).reply(200, t); + + const appDevTos = await tos.getAppDeveloperTOSStatus(testProjectId); + + expect(appDevTos).to.deep.equal(t); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getPublisherTOS", () => { + it("should get publisher TOS", async () => { + const t = testTOS("publishertos", "1.0.0"); + nock(api.extensionsTOSOrigin()) + .get(`/v1/projects/${testProjectId}/publishertos`) + .reply(200, t); + + const pubTos = await tos.getPublisherTOSStatus(testProjectId); + + expect(pubTos).to.deep.equal(t); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("acceptAppDeveloperTOS", () => { + it("should accept app dev TOS with no instance", async () => { + const t = testTOS("appdevtos", "1.0.0"); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/appdevtos:accept`) + .reply(200, t); + + const appDevTos = await tos.acceptAppDeveloperTOS(testProjectId, "1.0.0"); + + expect(appDevTos).to.deep.equal(t); + expect(nock.isDone()).to.be.true; + }); + + it("should accept app dev TOS with an instance", async () => { + const t = testTOS("appdevtos", "1.0.0"); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/appdevtos:accept`) + .reply(200, t); + + const appDevTos = await tos.acceptAppDeveloperTOS(testProjectId, "instanceId", "1.0.0"); + + expect(appDevTos).to.deep.equal(t); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("acceptPublisherTOS", () => { + it("should accept publisher TOS", async () => { + const t = testTOS("publishertos", "1.0.0"); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/publishertos:accept`) + .reply(200, t); + + const pubTos = await tos.acceptPublisherTOS(testProjectId, "1.0.0"); + + expect(pubTos).to.deep.equal(t); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("acceptLatestAppDeveloperTOS", () => { + it("should prompt to accept the latest app dev TOS if it has not been accepted", async () => { + const t = testTOS("appdevtos", "1.0.0"); + nock(api.extensionsTOSOrigin()).get(`/v1/projects/${testProjectId}/appdevtos`).reply(200, t); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/appdevtos:accept`) + .reply(200, t); + + const appDevTos = await tos.acceptLatestAppDeveloperTOS( + { + nonInteractive: true, + force: true, + }, + testProjectId, + ["my-instance"], + ); + + expect(appDevTos).to.deep.equal([t]); + expect(nock.isDone()).to.be.true; + }); + + it("should not prompt for the latest app dev TOS if it has already been accepted", async () => { + const t = testTOS("appdevtos", "1.1.0", "1.1.0"); + nock(api.extensionsTOSOrigin()).get(`/v1/projects/${testProjectId}/appdevtos`).reply(200, t); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/appdevtos:accept`) + .reply(200, t); + + const appDevTos = await tos.acceptLatestAppDeveloperTOS( + { + nonInteractive: true, + force: true, + }, + testProjectId, + ["my-instance"], + ); + + expect(appDevTos).to.deep.equal([t]); + expect(nock.isDone()).to.be.true; + }); + + it("should accept the TOS once per instance", async () => { + const t = testTOS("appdevtos", "1.1.0", "1.1.0"); + nock(api.extensionsTOSOrigin()).get(`/v1/projects/${testProjectId}/appdevtos`).reply(200, t); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/appdevtos:accept`) + .reply(200, t); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/appdevtos:accept`) + .reply(200, t); + + const appDevTos = await tos.acceptLatestAppDeveloperTOS( + { + nonInteractive: true, + force: true, + }, + testProjectId, + ["my-instance", "my-other-instance"], + ); + + expect(appDevTos).to.deep.equal([t, t]); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("acceptLatestPublisherTOS", () => { + it("should prompt to accept the latest publisher TOS if it has not been accepted", async () => { + const t = testTOS("publishertos", "1.0.0"); + nock(api.extensionsTOSOrigin()) + .get(`/v1/projects/${testProjectId}/publishertos`) + .reply(200, t); + nock(api.extensionsTOSOrigin()) + .post(`/v1/projects/${testProjectId}/publishertos:accept`) + .reply(200, t); + + const publisherTos = await tos.acceptLatestPublisherTOS( + { + nonInteractive: true, + force: true, + }, + testProjectId, + ); + + expect(publisherTos).to.deep.equal(t); + expect(nock.isDone()).to.be.true; + }); + }); + + it("should return the latest publisher TOS is it has already been accepted", async () => { + const t = testTOS("publishertos", "1.1.0", "1.1.0"); + nock(api.extensionsTOSOrigin()).get(`/v1/projects/${testProjectId}/publishertos`).reply(200, t); + + const publisherTos = await tos.acceptLatestPublisherTOS( + { + nonInteractive: true, + force: true, + }, + testProjectId, + ); + + expect(publisherTos).to.deep.equal(t); + expect(nock.isDone()).to.be.true; + }); +}); + +function testTOS(tosName: string, latestVersion: string, lastAcceptedVersion?: string): tos.TOS { + const t: tos.TOS = { + name: `projects/test-project/${tosName}`, + lastAcceptedTime: "11111", + latestTosVersion: latestVersion, + }; + if (lastAcceptedVersion) { + t.lastAcceptedVersion = lastAcceptedVersion; + } + return t; +} diff --git a/src/extensions/tos.ts b/src/extensions/tos.ts new file mode 100644 index 00000000000..eaa8e8ff7e7 --- /dev/null +++ b/src/extensions/tos.ts @@ -0,0 +1,140 @@ +import { Client } from "../apiv2"; +import { extensionsTOSOrigin } from "../api"; +import { logger } from "../logger"; +import { confirm } from "../prompt"; +import { FirebaseError } from "../error"; +import { logPrefix } from "./extensionsHelper"; +import * as utils from "../utils"; + +const VERSION = "v1"; +const extensionsTosUrl = (tos: string) => `https://firebase.google.com/terms/extensions/${tos}`; + +export interface TOS { + name: string; + lastAcceptedVersion?: string; + lastAcceptedTime?: string; + latestTosVersion: string; +} +export type PublisherTOS = TOS; +export type AppDevTOS = TOS; + +const apiClient = new Client({ urlPrefix: extensionsTOSOrigin(), apiVersion: VERSION }); + +export async function getAppDeveloperTOSStatus(projectId: string): Promise { + const res = await apiClient.get(`/projects/${projectId}/appdevtos`); + return res.body; +} + +export async function acceptAppDeveloperTOS( + projectId: string, + tosVersion: string, + instanceId: string = "", +): Promise { + const res = await apiClient.post< + { name: string; instanceId: string; version: string }, + AppDevTOS + >(`/projects/${projectId}/appdevtos:accept`, { + name: `project/${projectId}/appdevtos`, + instanceId, + version: tosVersion, + }); + return res.body; +} + +export async function getPublisherTOSStatus(projectId: string): Promise { + const res = await apiClient.get(`/projects/${projectId}/publishertos`); + return res.body; +} + +export async function acceptPublisherTOS( + projectId: string, + tosVersion: string, +): Promise { + const res = await apiClient.post<{ name: string; version: string }, PublisherTOS>( + `/projects/${projectId}/publishertos:accept`, + { + name: `project/${projectId}/publishertos`, + version: tosVersion, + }, + ); + return res.body; +} + +export async function acceptLatestPublisherTOS( + options: { force?: boolean; nonInteractive?: boolean }, + projectId: string, +): Promise { + try { + logger.debug(`Checking if latest publisher TOS has been accepted by ${projectId}...`); + const currentAcceptance = await getPublisherTOSStatus(projectId); + if (currentAcceptance.lastAcceptedVersion) { + logger.debug( + `Already accepted version ${currentAcceptance.lastAcceptedVersion} of Extensions publisher TOS.`, + ); + return currentAcceptance; + } else { + // Display link to TOS, prompt for acceptance + const tosLink = extensionsTosUrl("publisher"); + logger.info( + `To continue, you must accept the Firebase Extensions Publisher Terms of Service: ${tosLink}`, + ); + if ( + await confirm({ + ...options, + message: "Do you accept the Firebase Extensions Publisher Terms of Service?", + }) + ) { + return acceptPublisherTOS(projectId, currentAcceptance.latestTosVersion); + } + } + } catch (err: any) { + // This is a best effort check. When authenticated via a service account instead of OAuth, we cannot + // make calls to a private API. The extensions backend will also check TOS acceptance at instance CRUD time. + logger.debug( + `Error when checking Publisher TOS for ${projectId}. This is expected if authenticated via a service account: ${err}`, + ); + return; + } + throw new FirebaseError("You must accept the terms of service to continue."); +} + +export async function acceptLatestAppDeveloperTOS( + options: { force?: boolean; nonInteractive?: boolean }, + projectId: string, + instanceIds: string[], +): Promise { + try { + logger.debug(`Checking if latest AppDeveloper TOS has been accepted by ${projectId}...`); + displayDeveloperTOSWarning(); + const currentAcceptance = await getAppDeveloperTOSStatus(projectId); + if (currentAcceptance.lastAcceptedVersion) { + logger.debug(`User Terms of Service aready accepted on project ${projectId}.`); + } else if ( + !(await confirm({ + ...options, + message: "Do you accept the Firebase Extensions User Terms of Service?", + })) + ) { + throw new FirebaseError("You must accept the terms of service to continue."); + } + const tosPromises = instanceIds.map((instanceId) => { + return acceptAppDeveloperTOS(projectId, currentAcceptance.latestTosVersion, instanceId); + }); + return Promise.all(tosPromises); + } catch (err: any) { + // This is a best effort check. When authenticated via a service account instead of OAuth, we cannot + // make calls to a private API. The extensions backend will also check TOS acceptance at instance CRUD time. + logger.debug( + `Error when checking App Developer TOS for ${projectId}. This is expected if authenticated via a service account: ${err}`, + ); + return []; + } +} + +export function displayDeveloperTOSWarning(): void { + const tosLink = extensionsTosUrl("user"); + utils.logLabeledBullet( + logPrefix, + `By installing an extension instance onto a Firebase project, you accept the Firebase Extensions User Terms of Service: ${tosLink}`, + ); +} diff --git a/src/extensions/types.ts b/src/extensions/types.ts new file mode 100644 index 00000000000..fdb342a4db3 --- /dev/null +++ b/src/extensions/types.ts @@ -0,0 +1,260 @@ +import { MemoryOptions } from "../deploy/functions/backend"; +import { Runtime } from "../deploy/functions/runtimes/supported"; +import * as proto from "../gcp/proto"; +import { SpecParamType } from "./extensionsHelper"; + +export enum RegistryLaunchStage { + EXPERIMENTAL = "EXPERIMENTAL", + BETA = "BETA", + GA = "GA", + DEPRECATED = "DEPRECATED", + REGISTRY_LAUNCH_STAGE_UNSPECIFIED = "REGISTRY_LAUNCH_STAGE_UNSPECIFIED", +} + +export enum Visibility { + UNLISTED = "unlisted", + PUBLIC = "public", +} + +export interface Extension { + name: string; + ref: string; + state: ExtensionState; + visibility?: Visibility; + registryLaunchStage?: RegistryLaunchStage; + createTime: string; + latestApprovedVersion?: string; + latestVersion?: string; + latestVersionCreateTime?: string; + repoUri?: string; +} + +export interface Listing { + state: ListingState; +} + +export type ExtensionState = "STATE_UNSPECIFIED" | "PUBLISHED" | "DEPRECATED" | "SUSPENDED"; + +export type ListingState = "STATE_UPSPECIFIED" | "UNLISTED" | "PENDING" | "APPROVED" | "REJECTED"; + +export interface ExtensionVersion { + name: string; + ref: string; + state: "STATE_UNSPECIFIED" | "PUBLISHED" | "DEPRECATED"; + spec: ExtensionSpec; + hash: string; + sourceDownloadUri: string; + buildSourceUri?: string; + releaseNotes?: string; + createTime?: string; + deprecationMessage?: string; + extensionRoot?: string; + listing?: Listing; +} + +export interface PublisherProfile { + name: string; + publisherId: string; + registerTime: string; + displayName: string; + websiteUri?: string; + iconUri?: string; +} + +export interface ExtensionInstance { + name: string; + createTime: string; + updateTime: string; + state: "STATE_UNSPECIFIED" | "DEPLOYING" | "UNINSTALLING" | "ACTIVE" | "ERRORED" | "PAUSED"; + config: ExtensionConfig; + serviceAccountEmail: string; + errorStatus?: string; + lastOperationName?: string; + lastOperationType?: string; + etag?: string; + extensionRef?: string; + extensionVersion?: string; +} + +export interface ExtensionConfig { + name: string; + createTime: string; + source: ExtensionSource; + params: Record; + systemParams: Record; + populatedPostinstallContent?: string; + extensionRef?: string; + extensionVersion?: string; + eventarcChannel?: string; + allowedEventTypes?: string[]; +} + +export interface ExtensionSource { + state: "STATE_UNSPECIFIED" | "ACTIVE" | "DELETED"; + name: string; + packageUri: string; + hash: string; + spec: ExtensionSpec; + extensionRoot?: string; + fetchTime?: string; + lastOperationName?: string; +} + +export interface ExtensionSpec { + specVersion?: string; + name: string; + version: string; + displayName?: string; + description?: string; + apis?: Api[]; + roles?: Role[]; + resources: Resource[]; + billingRequired?: boolean; + author?: Author; + contributors?: Author[]; + license?: string; + releaseNotesUrl?: string; + sourceUrl?: string; + params: Param[]; + systemParams: Param[]; + preinstallContent?: string; + postinstallContent?: string; + readmeContent?: string; + externalServices?: ExternalService[]; + events?: EventDescriptor[]; + lifecycleEvents?: LifecycleEvent[]; +} + +export interface LifecycleEvent { + stage: "STAGE_UNSPECIFIED" | "ON_INSTALL" | "ON_UPDATE" | "ON_CONFIGURE"; + taskQueueTriggerFunction: string; +} + +export interface EventDescriptor { + type: string; + description: string; +} + +export interface ExternalService { + name: string; + pricingUri: string; +} + +export interface Api { + apiName: string; + reason: string; +} + +export interface Role { + role: string; + reason: string; +} + +// Docs at https://firebase.google.com/docs/extensions/reference/extension-yaml +export const FUNCTIONS_RESOURCE_TYPE = "firebaseextensions.v1beta.function"; +export interface FunctionResourceProperties { + type: typeof FUNCTIONS_RESOURCE_TYPE; + properties?: { + location?: string; + entryPoint?: string; + sourceDirectory?: string; + timeout?: proto.Duration; + availableMemoryMb?: MemoryOptions; + runtime?: Runtime; + httpsTrigger?: Record; + scheduleTrigger?: Record; + taskQueueTrigger?: { + rateLimits?: { + maxConcurrentDispatchs?: number; + maxDispatchesPerSecond?: number; + }; + retryConfig?: { + maxAttempts?: number; + maxRetrySeconds?: number; + maxBackoffSeconds?: number; + maxDoublings?: number; + minBackoffSeconds?: number; + }; + }; + eventTrigger?: { + eventType: string; + resource: string; + service?: string; + }; + }; +} + +export const FUNCTIONS_V2_RESOURCE_TYPE = "firebaseextensions.v1beta.v2function"; +export interface FunctionV2ResourceProperties { + type: typeof FUNCTIONS_V2_RESOURCE_TYPE; + properties?: { + location?: string; + sourceDirectory?: string; + buildConfig?: { + runtime?: Runtime; + }; + serviceConfig?: { + availableMemory?: string; + timeoutSeconds?: number; + minInstanceCount?: number; + maxInstanceCount?: number; + }; + eventTrigger?: { + eventType: string; + triggerRegion?: string; + channel?: string; + pubsubTopic?: string; + retryPolicy?: string; + eventFilters?: FunctionV2EventFilter[]; + }; + }; +} + +export interface FunctionV2EventFilter { + attribute: string; + value: string; + operator?: string; +} + +// Union of all valid property types so we can have a strongly typed "property" +// field depending on the actual value of "type" +type ResourceProperties = FunctionResourceProperties | FunctionV2ResourceProperties; + +export type Resource = ResourceProperties & { + name: string; + description?: string; + propertiesYaml?: string; + entryPoint?: string; +}; + +export interface Author { + authorName: string; + url?: string; +} + +export interface Param { + param: string; // The key of the {param:value} pair. + label: string; + description?: string; + default?: string; + type?: ParamType | SpecParamType; // TODO(b/224618262): This is SpecParamType when publishing & ParamType when looking at API responses. Choose one. + options?: ParamOption[]; + required?: boolean; + validationRegex?: string; + validationErrorMessage?: string; + immutable?: boolean; + example?: string; + advanced?: boolean; +} + +export enum ParamType { + STRING = "STRING", + SELECT = "SELECT", + MULTISELECT = "MULTISELECT", + SECRET = "SECRET", +} + +export interface ParamOption { + value: string; + label?: string; +} diff --git a/src/extensions/updateHelper.spec.ts b/src/extensions/updateHelper.spec.ts new file mode 100644 index 00000000000..9f508a5ad75 --- /dev/null +++ b/src/extensions/updateHelper.spec.ts @@ -0,0 +1,244 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as sinon from "sinon"; + +import { FirebaseError } from "../error"; +import { firebaseExtensionsRegistryOrigin } from "../api"; +import * as extensionsApi from "./extensionsApi"; +import { ExtensionSpec, Resource } from "./types"; +import * as extensionsHelper from "./extensionsHelper"; +import * as updateHelper from "./updateHelper"; +import * as iam from "../gcp/iam"; + +const SPEC: ExtensionSpec = { + name: "test", + displayName: "Old", + description: "descriptive", + version: "0.2.0", + license: "MIT", + apis: [ + { apiName: "api1", reason: "" }, + { apiName: "api2", reason: "" }, + ], + roles: [ + { role: "role1", reason: "" }, + { role: "role2", reason: "" }, + ], + resources: [ + { name: "resource1", type: "firebaseextensions.v1beta.function", description: "desc" }, + { name: "resource2", type: "other", description: "" } as unknown as Resource, + ], + author: { authorName: "Tester" }, + contributors: [{ authorName: "Tester 2" }], + billingRequired: true, + sourceUrl: "test.com", + params: [], + systemParams: [], +}; + +const SOURCE = { + name: "projects/firebasemods/sources/new-test-source", + packageUri: "https://firebase-fake-bucket.com", + hash: "1234567", + spec: SPEC, +}; + +const INSTANCE = { + name: "projects/invader-zim/instances/instance-of-official-ext", + createTime: "2019-05-19T00:20:10.416947Z", + updateTime: "2019-05-19T00:20:10.416947Z", + state: "ACTIVE", + config: { + name: "projects/invader-zim/instances/instance-of-official-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + sourceId: "fake-official-source", + sourceName: "projects/firebasemods/sources/fake-official-source", + source: { + name: "projects/firebasemods/sources/fake-official-source", + }, + }, +}; + +const REGISTRY_INSTANCE = { + name: "projects/invader-zim/instances/instance-of-registry-ext", + createTime: "2019-05-19T00:20:10.416947Z", + updateTime: "2019-05-19T00:20:10.416947Z", + state: "ACTIVE", + config: { + name: "projects/invader-zim/instances/instance-of-registry-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + sourceId: "fake-registry-source", + sourceName: "projects/firebasemods/sources/fake-registry-source", + extensionRef: "test-publisher/test", + source: { + name: "projects/firebasemods/sources/fake-registry-source", + }, + }, +}; + +const LOCAL_INSTANCE = { + name: "projects/invader-zim/instances/instance-of-local-ext", + createTime: "2019-05-19T00:20:10.416947Z", + updateTime: "2019-05-19T00:20:10.416947Z", + state: "ACTIVE", + config: { + name: "projects/invader-zim/instances/instance-of-local-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + sourceId: "fake-registry-source", + sourceName: "projects/firebasemods/sources/fake-local-source", + source: { + name: "projects/firebasemods/sources/fake-local-source", + }, + }, +}; + +describe("updateHelper", () => { + describe("updateFromLocalSource", () => { + let createSourceStub: sinon.SinonStub; + let getInstanceStub: sinon.SinonStub; + let getRoleStub: sinon.SinonStub; + beforeEach(() => { + createSourceStub = sinon.stub(extensionsHelper, "createSourceFromLocation"); + getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(INSTANCE); + getRoleStub = sinon.stub(iam, "getRole"); + getRoleStub.resolves({ + title: "Role 1", + description: "a role", + }); + // The logic will fetch the extensions registry, but it doesn't need to receive anything. + nock(firebaseExtensionsRegistryOrigin()).get("/extensions.json").reply(200, {}); + }); + + afterEach(() => { + createSourceStub.restore(); + getInstanceStub.restore(); + getRoleStub.restore(); + + nock.cleanAll(); + }); + + it("should return the correct source name for a valid local source", async () => { + createSourceStub.resolves(SOURCE); + const name = await updateHelper.updateFromLocalSource( + "test-project", + "test-instance", + ".", + SPEC, + ); + expect(name).to.equal(SOURCE.name); + }); + + it("should throw an error for an invalid source", async () => { + createSourceStub.throwsException("Invalid source"); + await expect( + updateHelper.updateFromLocalSource("test-project", "test-instance", ".", SPEC), + ).to.be.rejectedWith(FirebaseError, "Unable to update from the source"); + }); + }); + + describe("updateFromUrlSource", () => { + let createSourceStub: sinon.SinonStub; + let getInstanceStub: sinon.SinonStub; + let getRoleStub: sinon.SinonStub; + beforeEach(() => { + createSourceStub = sinon.stub(extensionsHelper, "createSourceFromLocation"); + getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(INSTANCE); + getRoleStub = sinon.stub(iam, "getRole"); + getRoleStub.resolves({ + title: "Role 1", + description: "a role", + }); + // The logic will fetch the extensions registry, but it doesn't need to receive anything. + nock(firebaseExtensionsRegistryOrigin()).get("/extensions.json").reply(200, {}); + }); + + afterEach(() => { + createSourceStub.restore(); + getInstanceStub.restore(); + getRoleStub.restore(); + + nock.cleanAll(); + }); + + it("should return the correct source name for a valid url source", async () => { + createSourceStub.resolves(SOURCE); + const name = await updateHelper.updateFromUrlSource( + "test-project", + "test-instance", + "https://valid-source.tar.gz", + SPEC, + ); + expect(name).to.equal(SOURCE.name); + }); + + it("should throw an error for an invalid source", async () => { + createSourceStub.throws("Invalid source"); + await expect( + updateHelper.updateFromUrlSource( + "test-project", + "test-instance", + "https://valid-source.tar.gz", + SPEC, + ), + ).to.be.rejectedWith(FirebaseError, "Unable to update from the source"); + }); + }); +}); + +describe("inferUpdateSource", () => { + it("should infer update source from ref without version", () => { + const result = updateHelper.inferUpdateSource("", "firebase/storage-resize-images"); + expect(result).to.equal("firebase/storage-resize-images@latest"); + }); + + it("should infer update source from ref with just version", () => { + const result = updateHelper.inferUpdateSource("0.1.2", "firebase/storage-resize-images"); + expect(result).to.equal("firebase/storage-resize-images@0.1.2"); + }); + + it("should infer update source from ref and extension name", () => { + const result = updateHelper.inferUpdateSource( + "storage-resize-images", + "firebase/storage-resize-images", + ); + expect(result).to.equal("firebase/storage-resize-images@latest"); + }); + + it("should infer update source if it is a ref distinct from the input ref", () => { + const result = updateHelper.inferUpdateSource( + "notfirebase/storage-resize-images", + "firebase/storage-resize-images", + ); + expect(result).to.equal("notfirebase/storage-resize-images@latest"); + }); +}); + +describe("getExistingSourceOrigin", () => { + let getInstanceStub: sinon.SinonStub; + + afterEach(() => { + getInstanceStub.restore(); + }); + + it("should return published extension as source origin", async () => { + getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(REGISTRY_INSTANCE); + + const result = await updateHelper.getExistingSourceOrigin( + "invader-zim", + "instance-of-registry-ext", + ); + + expect(result).to.equal(extensionsHelper.SourceOrigin.PUBLISHED_EXTENSION); + }); + + it("should return local extension as source origin", async () => { + getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(LOCAL_INSTANCE); + + const result = await updateHelper.getExistingSourceOrigin( + "invader-zim", + "instance-of-local-ext", + ); + + expect(result).to.equal(extensionsHelper.SourceOrigin.LOCAL); + }); +}); diff --git a/src/extensions/updateHelper.ts b/src/extensions/updateHelper.ts index c8e189e7d90..d4641d8a0d9 100644 --- a/src/extensions/updateHelper.ts +++ b/src/extensions/updateHelper.ts @@ -1,12 +1,11 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as semver from "semver"; -import * as marked from "marked"; +import { marked } from "marked"; import { FirebaseError } from "../error"; import { logger } from "../logger"; -import * as resolveSource from "./resolveSource"; import * as extensionsApi from "./extensionsApi"; -import * as refs from "./refs"; +import { ExtensionSource, ExtensionSpec } from "./types"; import { createSourceFromLocation, logPrefix, @@ -14,28 +13,21 @@ import { isLocalOrURLPath, } from "./extensionsHelper"; import * as utils from "../utils"; -import { - displayUpdateChangesNoInput, - displayUpdateChangesRequiringConfirmation, - displayExtInfo, -} from "./displayExtensionInfo"; -import * as changelog from "./changelog"; +import { displayExtensionVersionInfo } from "./displayExtensionInfo"; function invalidSourceErrMsgTemplate(instanceId: string, source: string): string { return `Unable to update from the source \`${clc.bold( - source + source, )}\`. To update this instance, you can either:\n - Run \`${clc.bold("firebase ext:update " + instanceId)}\` to update from the published source.\n - Check your directory path or URL, then run \`${clc.bold( - "firebase ext:update " + instanceId + " " + "firebase ext:update " + instanceId + " ", )}\` to update from a local directory or URL source.`; } export async function getExistingSourceOrigin( projectId: string, instanceId: string, - extensionName: string, - existingSource: string ): Promise { const instance = await extensionsApi.getInstance(projectId, instanceId); return instance && instance.config.extensionRef @@ -47,7 +39,7 @@ function showUpdateVersionInfo( instanceId: string, from: string, to: string, - source?: string + source?: string, ): void { if (source) { source = clc.bold(source); @@ -56,12 +48,12 @@ function showUpdateVersionInfo( } utils.logLabeledBullet( logPrefix, - `Updating ${clc.bold(instanceId)} from version ${clc.bold(from)} to ${source} (${clc.bold(to)})` + `Updating ${clc.bold(instanceId)} from version ${clc.bold(from)} to ${source} (${clc.bold(to)})`, ); if (semver.lt(to, from)) { utils.logLabeledWarning( logPrefix, - "The version you are updating to is less than the current version for this extension. This extension may not be backwards compatible." + "The version you are updating to is less than the current version for this extension. This extension may not be backwards compatible.", ); } return; @@ -75,7 +67,7 @@ export function warningUpdateToOtherSource(sourceOrigin: SourceOrigin) { let targetText; if ( [SourceOrigin.PUBLISHED_EXTENSION, SourceOrigin.PUBLISHED_EXTENSION_VERSION].includes( - sourceOrigin + sourceOrigin, ) ) { targetText = "published extension"; @@ -88,26 +80,6 @@ export function warningUpdateToOtherSource(sourceOrigin: SourceOrigin) { logger.info(marked(warning)); } -/** - * Displays all differences between spec and newSpec. - * First, displays all changes that do not require explicit confirmation, - * then prompts the user for each change that requires confirmation. - * - * @param spec A current extensionSpec - * @param newSpec A extensionSpec to compare to - * @param published - */ -export async function displayChanges(args: { - spec: extensionsApi.ExtensionSpec; - newSpec: extensionsApi.ExtensionSpec; - nonInteractive: boolean; - force: boolean; -}): Promise { - utils.logLabeledBullet("extensions", "This update contains the following changes:"); - displayUpdateChangesNoInput(args.spec, args.newSpec); - await displayUpdateChangesRequiringConfirmation(args); -} - /** * @param projectId Id of the project containing the instance to update * @param instanceId Id of the instance to update @@ -119,9 +91,12 @@ export async function displayChanges(args: { export interface UpdateOptions { projectId: string; instanceId: string; - source?: extensionsApi.ExtensionSource; + source?: ExtensionSource; extRef?: string; params?: { [key: string]: string }; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; } /** @@ -133,13 +108,25 @@ export interface UpdateOptions { * @param updateOptions Info on the instance and associated resources to update */ export async function update(updateOptions: UpdateOptions): Promise { - const { projectId, instanceId, source, extRef, params } = updateOptions; + const { + projectId, + instanceId, + source, + extRef, + params, + canEmitEvents, + allowedEventTypes, + eventarcChannel, + } = updateOptions; if (extRef) { return await extensionsApi.updateInstanceFromRegistry({ projectId, instanceId, extRef, params, + canEmitEvents, + allowedEventTypes, + eventarcChannel, }); } else if (source) { return await extensionsApi.updateInstance({ @@ -147,10 +134,13 @@ export async function update(updateOptions: UpdateOptions): Promise { instanceId, extensionSource: source, params, + canEmitEvents, + allowedEventTypes, + eventarcChannel, }); } throw new FirebaseError( - `Neither a source nor a version of the extension was supplied for ${instanceId}. Please make sure this is a valid extension and try again.` + `Neither a source nor a version of the extension was supplied for ${instanceId}. Please make sure this is a valid extension and try again.`, ); } @@ -165,18 +155,18 @@ export async function updateFromLocalSource( projectId: string, instanceId: string, localSource: string, - existingSpec: extensionsApi.ExtensionSpec + existingSpec: ExtensionSpec, ): Promise { - displayExtInfo(instanceId, "", existingSpec, false); + await displayExtensionVersionInfo({ spec: existingSpec }); let source; try { source = await createSourceFromLocation(projectId, localSource); - } catch (err) { + } catch (err: any) { throw new FirebaseError(invalidSourceErrMsgTemplate(instanceId, localSource)); } utils.logLabeledBullet( logPrefix, - `${clc.bold("You are updating this extension instance to a local source.")}` + `${clc.bold("You are updating this extension instance to a local source.")}`, ); showUpdateVersionInfo(instanceId, existingSpec.version, source.spec.version, localSource); warningUpdateToOtherSource(SourceOrigin.LOCAL); @@ -195,104 +185,24 @@ export async function updateFromUrlSource( projectId: string, instanceId: string, urlSource: string, - existingSpec: extensionsApi.ExtensionSpec + existingSpec: ExtensionSpec, ): Promise { - displayExtInfo(instanceId, "", existingSpec, false); + await displayExtensionVersionInfo({ spec: existingSpec }); let source; try { source = await createSourceFromLocation(projectId, urlSource); - } catch (err) { + } catch (err: any) { throw new FirebaseError(invalidSourceErrMsgTemplate(instanceId, urlSource)); } utils.logLabeledBullet( logPrefix, - `${clc.bold("You are updating this extension instance to a URL source.")}` + `${clc.bold("You are updating this extension instance to a URL source.")}`, ); showUpdateVersionInfo(instanceId, existingSpec.version, source.spec.version, urlSource); warningUpdateToOtherSource(SourceOrigin.URL); return source.name; } -/** - * @param instanceId Id of the instance to update - * @param extVersionRef extension reference of extension source to update to (publisherId/extensionId@versionId) - * @param existingSpec ExtensionSpec of existing instance source - * @param existingSource name of existing instance source - */ -export async function updateToVersionFromPublisherSource( - projectId: string, - instanceId: string, - extVersionRef: string, - existingSpec: extensionsApi.ExtensionSpec -): Promise { - let source; - const ref = refs.parse(extVersionRef); - const version = ref.version; - const extensionRef = refs.toExtensionRef(ref); - displayExtInfo(instanceId, ref.publisherId, existingSpec, true); - const extension = await extensionsApi.getExtension(extensionRef); - try { - source = await extensionsApi.getExtensionVersion(extVersionRef); - } catch (err) { - throw new FirebaseError( - `Could not find source '${clc.bold(extVersionRef)}' because (${clc.bold( - version - )}) is not a published version. To update, use the latest version of this extension (${clc.bold( - extension.latestVersion - )}).` - ); - } - let registryEntry; - try { - registryEntry = await resolveSource.resolveRegistryEntry(existingSpec.name); - } catch (err) { - logger.debug(`Unable to fetch registry.json entry for ${existingSpec.name}`); - } - - if (registryEntry) { - // Do not allow user to "downgrade" to a version lower than the minimum required version. - const minVer = resolveSource.getMinRequiredVersion(registryEntry); - if (minVer && semver.gt(minVer, source.spec.version)) { - throw new FirebaseError( - `The version you are trying to update to (${clc.bold( - source.spec.version - )}) is less than the minimum version required (${clc.bold(minVer)}) to use this extension.` - ); - } - } - showUpdateVersionInfo(instanceId, existingSpec.version, source.spec.version, extVersionRef); - warningUpdateToOtherSource(SourceOrigin.PUBLISHED_EXTENSION); - const releaseNotes = await changelog.getReleaseNotesForUpdate({ - extensionRef, - fromVersion: existingSpec.version, - toVersion: source.spec.version, - }); - if (Object.keys(releaseNotes).length) { - changelog.displayReleaseNotes(releaseNotes, existingSpec.version); - } - return source.name; -} - -/** - * @param instanceId Id of the instance to update - * @param extRef extension reference of extension source to update to (publisherId/extensionId) - * @param existingSpec ExtensionSpec of existing instance source - * @param existingSource name of existing instance source - */ -export async function updateFromPublisherSource( - projectId: string, - instanceId: string, - extRef: string, - existingSpec: extensionsApi.ExtensionSpec -): Promise { - return updateToVersionFromPublisherSource( - projectId, - instanceId, - `${extRef}@latest`, - existingSpec - ); -} - export function inferUpdateSource(updateSource: string, existingRef: string): string { if (!updateSource) { return `${existingRef}@latest`; diff --git a/src/extensions/utils.spec.ts b/src/extensions/utils.spec.ts new file mode 100644 index 00000000000..91750330629 --- /dev/null +++ b/src/extensions/utils.spec.ts @@ -0,0 +1,11 @@ +import { expect } from "chai"; + +import * as utils from "./utils"; + +describe("extensions utils", () => { + describe("formatTimestamp", () => { + it("should format timestamp correctly", () => { + expect(utils.formatTimestamp("2020-05-11T03:45:13.583677Z")).to.equal("2020-05-11 03:45:13"); + }); + }); +}); diff --git a/src/extensions/utils.ts b/src/extensions/utils.ts index cb6ffaa4594..71a0c715614 100644 --- a/src/extensions/utils.ts +++ b/src/extensions/utils.ts @@ -1,9 +1,16 @@ -import * as _ from "lodash"; import { promptOnce } from "../prompt"; -import { ParamOption } from "./extensionsApi"; +import { + ParamOption, + Resource, + FUNCTIONS_RESOURCE_TYPE, + FUNCTIONS_V2_RESOURCE_TYPE, +} from "./types"; import { RegistryEntry } from "./resolveSource"; +import { Runtime } from "../deploy/functions/runtimes/supported"; -// Modified version of the once function from prompt, to return as a joined string. +/** + * Modified version of the once function from prompt, to return as a joined string. + */ export async function onceWithJoin(question: any): Promise { const response = await promptOnce(question); if (Array.isArray(response)) { @@ -18,29 +25,33 @@ interface ListItem { checked: boolean; // Whether the option should be checked by default } -// Convert extension option to Inquirer-friendly list for the prompt, with all items unchecked. +/** + * Convert extension option to Inquirer-friendly list for the prompt, with all items unchecked. + */ export function convertExtensionOptionToLabeledList(options: ParamOption[]): ListItem[] { - return options.map( - (option: ParamOption): ListItem => { - return { - checked: false, - name: option.label, - value: option.value, - }; - } - ); + return options.map((option: ParamOption): ListItem => { + return { + checked: false, + name: option.label, + value: option.value, + }; + }); } -// Convert map of RegistryEntry into Inquirer-friendly list for prompt, with all items unchecked. +/** + * Convert map of RegistryEntry into Inquirer-friendly list for prompt, with all items unchecked. + */ export function convertOfficialExtensionsToList(officialExts: { [key: string]: RegistryEntry; }): ListItem[] { - return _.map(officialExts, (entry: RegistryEntry, key: string) => { + const l = Object.entries(officialExts).map(([key, entry]) => { return { checked: false, - value: key, + value: `${entry.publisher}/${key}`, }; }); + l.sort((a, b) => a.value.localeCompare(b.value)); + return l; } /** @@ -68,3 +79,19 @@ export function formatTimestamp(timestamp: string): string { const withoutMs = timestamp.split(".")[0]; return withoutMs.replace("T", " "); } + +/** + * Returns the runtime for the resource. The resource may be v1 or v2 function, + * etc, and this utility will do its best to identify the runtime specified for + * this resource. + */ +export function getResourceRuntime(resource: Resource): Runtime | undefined { + switch (resource.type) { + case FUNCTIONS_RESOURCE_TYPE: + return resource.properties?.runtime; + case FUNCTIONS_V2_RESOURCE_TYPE: + return resource.properties?.buildConfig?.runtime; + default: + return undefined; + } +} diff --git a/src/extensions/warnings.spec.ts b/src/extensions/warnings.spec.ts new file mode 100644 index 00000000000..8dc05262261 --- /dev/null +++ b/src/extensions/warnings.spec.ts @@ -0,0 +1,103 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as warnings from "./warnings"; +import { + Extension, + ExtensionVersion, + ListingState, + RegistryLaunchStage, + Visibility, +} from "./types"; +import { DeploymentInstanceSpec } from "../deploy/extensions/planner"; +import * as utils from "../utils"; + +const testExtensionVersion = (listingState: ListingState): ExtensionVersion => { + return { + name: "test", + ref: "test/test@0.1.0", + state: "PUBLISHED", + hash: "abc123", + sourceDownloadUri: "https://download.com/source", + spec: { + name: "test", + version: "0.1.0", + resources: [], + params: [], + systemParams: [], + sourceUrl: "github.com/test/meout", + }, + listing: { + state: listingState, + }, + }; +}; + +const testExtension = (publisherId: string): Extension => { + return { + name: "test", + state: "PUBLISHED", + ref: `${publisherId}/test`, + registryLaunchStage: RegistryLaunchStage.BETA, + createTime: "101", + visibility: Visibility.PUBLIC, + }; +}; + +const testInstanceSpec = ( + publisherId: string, + instanceId: string, + listingState: ListingState, +): DeploymentInstanceSpec => { + return { + instanceId, + ref: { + publisherId, + extensionId: "test", + version: "0.1.0", + }, + params: {}, + systemParams: {}, + extensionVersion: testExtensionVersion(listingState), + extension: testExtension(publisherId), + }; +}; + +describe("displayWarningsForDeploy", () => { + let loggerStub: sinon.SinonStub; + + beforeEach(() => { + loggerStub = sinon.stub(utils, "logLabeledBullet"); + }); + + afterEach(() => { + loggerStub.restore(); + }); + + it("should not warn if published", async () => { + const toCreate = [ + testInstanceSpec("firebase", "ext-id-1", "APPROVED"), + testInstanceSpec("firebase", "ext-id-2", "APPROVED"), + ]; + + const warned = await warnings.displayWarningsForDeploy(toCreate); + + expect(warned).to.be.false; + expect(loggerStub).to.not.have.been.called; + }); + + it("should not warn if not published", async () => { + const toCreate = [ + testInstanceSpec("pubby-mcpublisher", "ext-id-1", "PENDING"), + testInstanceSpec("pubby-mcpublisher", "ext-id-2", "REJECTED"), + ]; + + const warned = await warnings.displayWarningsForDeploy(toCreate); + + expect(warned).to.be.true; + expect(loggerStub).to.have.been.calledWithMatch( + "extensions", + "have not been published to the Firebase Extensions Hub", + ); + }); +}); diff --git a/src/extensions/warnings.ts b/src/extensions/warnings.ts index eae3d15dad4..441f6b535d2 100644 --- a/src/extensions/warnings.ts +++ b/src/extensions/warnings.ts @@ -1,68 +1,21 @@ -import * as marked from "marked"; -import * as clc from "cli-color"; +import * as clc from "colorette"; +import * as TerminalRenderer from "marked-terminal"; +import { marked } from "marked"; +marked.setOptions({ + renderer: new TerminalRenderer(), +}); -import { ExtensionVersion, RegistryLaunchStage } from "./extensionsApi"; -import { printSourceDownloadLink } from "./displayExtensionInfo"; import { logPrefix } from "./extensionsHelper"; -import { getTrustedPublishers } from "./resolveSource"; import { humanReadable } from "../deploy/extensions/deploymentSummary"; -import { InstanceSpec, getExtension, getExtensionVersion } from "../deploy/extensions/planner"; -import { partition } from "../functional"; +import { InstanceSpec, getExtensionVersion } from "../deploy/extensions/planner"; +import { logger } from "../logger"; import * as utils from "../utils"; -interface displayEAPWarningParameters { - publisherId: string; - sourceDownloadUri: string; - githubLink?: string; -} - -function displayEAPWarning({ - publisherId, - sourceDownloadUri, - githubLink, -}: displayEAPWarningParameters): void { - const publisherNameLink = githubLink ? `[${publisherId}](${githubLink})` : publisherId; - const warningMsg = `This extension is in preview and is built by a developer in the [Extensions Publisher Early Access Program](http://bit.ly/firex-provider). Its functionality might change in backward-incompatible ways. Since this extension isn't built by Firebase, reach out to ${publisherNameLink} with questions about this extension.`; - const legalMsg = - "\n\nIt is provided “AS IS”, without any warranty, express or implied, from Google. Google disclaims all liability for any damages, direct or indirect, resulting from the use of the extension, and its functionality might change in backward - incompatible ways."; - utils.logLabeledBullet(logPrefix, marked(warningMsg + legalMsg)); - printSourceDownloadLink(sourceDownloadUri); -} - -function displayExperimentalWarning() { - utils.logLabeledBullet( - logPrefix, - marked( - `${clc.yellow.bold("Important")}: This extension is ${clc.bold( - "experimental" - )} and may not be production-ready. Its functionality might change in backward-incompatible ways before its official release, or it may be discontinued.` - ) - ); -} - -export async function displayWarningPrompts( - publisherId: string, - launchStage: RegistryLaunchStage, - extensionVersion: ExtensionVersion -): Promise { - const trustedPublishers = await getTrustedPublishers(); - if (!trustedPublishers.includes(publisherId)) { - displayEAPWarning({ - publisherId, - sourceDownloadUri: extensionVersion.sourceDownloadUri, - githubLink: extensionVersion.spec.sourceUrl, - }); - } else if (launchStage === RegistryLaunchStage.EXPERIMENTAL) { - displayExperimentalWarning(); - } else { - // Otherwise, this is an official extension and requires no warning prompts. - return; - } -} - const toListEntry = (i: InstanceSpec) => { const idAndRef = humanReadable(i); - const sourceCodeLink = `\n\t[Source Code](${i.extensionVersion?.sourceDownloadUri})`; + const sourceCodeLink = `\n\t[Source Code](${ + i.extensionVersion?.buildSourceUri ?? i.extensionVersion?.sourceDownloadUri + })`; const githubLink = i.extensionVersion?.spec?.sourceUrl ? `\n\t[Publisher Contact](${i.extensionVersion?.spec.sourceUrl})` : ""; @@ -75,43 +28,33 @@ const toListEntry = (i: InstanceSpec) => { * @param instancesToCreate A list of instances that will be created in this deploy */ export async function displayWarningsForDeploy(instancesToCreate: InstanceSpec[]) { - const trustedPublishers = await getTrustedPublishers(); - for (const i of instancesToCreate) { - await getExtension(i); + const uploadedExtensionInstances = instancesToCreate.filter((i) => i.ref); + for (const i of uploadedExtensionInstances) { await getExtensionVersion(i); } - - const [eapExtensions, nonEapExtensions] = partition( - instancesToCreate, - (i) => !trustedPublishers.includes(i.ref?.publisherId ?? "") - ); - // Only mark non-eap extensions as expeirmental. - const experimental = nonEapExtensions.filter( - (i) => i.extension!.registryLaunchStage === RegistryLaunchStage.EXPERIMENTAL + const unpublishedExtensions = uploadedExtensionInstances.filter( + (i) => i.extensionVersion?.listing?.state !== "APPROVED", ); - if (experimental.length) { - const humanReadableList = experimental.map((i) => `\t${humanReadable(i)}`).join("\n"); + if (unpublishedExtensions.length) { + const humanReadableList = unpublishedExtensions.map(toListEntry).join("\n"); utils.logLabeledBullet( logPrefix, marked( - `The following are instances of ${clc.bold( - "experimental" - )} extensions.They may not be production-ready. Their functionality may change in backward-incompatible ways before their official release, or they may be discontinued.\n${humanReadableList}\n` - ) + `The following extension versions have not been published to the Firebase Extensions Hub:\n${humanReadableList}\n.` + + "Unpublished extensions have not been reviewed by " + + "Firebase. Please make sure you trust the extension publisher before installing this extension.", + { gfm: false }, + ), ); } + return unpublishedExtensions.length > 0; +} - if (eapExtensions.length) { - const humanReadableList = eapExtensions.map(toListEntry).join("\n"); - utils.logLabeledBullet( - logPrefix, - marked( - `These extensions are in preview and are built by a developer in the Extensions Publisher Early Access Program (http://bit.ly/firex-provider. Their functionality might change in backwards-incompatible ways. Since these extensions aren't built by Firebase, reach out to their publisher with questions about them.` + - ` They are provided “AS IS”, without any warranty, express or implied, from Google.` + - ` Google disclaims all liability for any damages, direct or indirect, resulting from the use of these extensions\n${humanReadableList}` - ) - ); - } - return experimental.length > 0 || eapExtensions.length > 0; +export function outOfBandChangesWarning(instanceIds: string[]) { + logger.warn( + "The following instances may have been changed in the Firebase console or by another machine since the last deploy from this machine.\n\t" + + clc.bold(instanceIds.join("\n\t")) + + "\nIf you proceed with this deployment, those changes will be overwritten. To avoid this, run `firebase ext:export` to sync these changes to your local extensions manifest.", + ); } diff --git a/src/fetchMOTD.ts b/src/fetchMOTD.ts index f535d11aef7..335e309b101 100644 --- a/src/fetchMOTD.ts +++ b/src/fetchMOTD.ts @@ -1,8 +1,9 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as semver from "semver"; import { Client } from "./apiv2"; import { configstore } from "./configstore"; +import { logger } from "./logger"; import { realtimeOrigin } from "./api"; import * as utils from "./utils"; @@ -26,7 +27,7 @@ export function fetchMOTD(): void { ", need at least", clc.bold(motd.minVersion) + ")\n\nRun", clc.bold("npm install -g firebase-tools"), - "to upgrade." + "to upgrade.", ); process.exit(1); } @@ -41,12 +42,19 @@ export function fetchMOTD(): void { } } } else { - const origin = utils.addSubdomain(realtimeOrigin, "firebase-public"); + const origin = utils.addSubdomain(realtimeOrigin(), "firebase-public"); const c = new Client({ urlPrefix: origin, auth: false }); - c.get("/cli.json").then((res) => { - motd = Object.assign({}, res.body); - configstore.set("motd", motd); - configstore.set("motd.fetched", Date.now()); - }); + c.get("/cli.json") + .then((res) => { + motd = Object.assign({}, res.body); + configstore.set("motd", motd); + configstore.set("motd.fetched", Date.now()); + }) + .catch((err) => { + utils.logWarning( + "Unable to fetch the CLI MOTD and remote config. This is not a fatal error, but may indicate an issue with your network connection.", + ); + logger.debug(`Failed to fetch MOTD ${err}`); + }); } } diff --git a/src/test/fetchWebSetup.spec.ts b/src/fetchWebSetup.spec.ts similarity index 87% rename from src/test/fetchWebSetup.spec.ts rename to src/fetchWebSetup.spec.ts index 55ae6dc49db..acb6fe3c878 100644 --- a/src/test/fetchWebSetup.spec.ts +++ b/src/fetchWebSetup.spec.ts @@ -2,12 +2,20 @@ import { expect } from "chai"; import * as nock from "nock"; import * as sinon from "sinon"; -import { configstore } from "../configstore"; -import { fetchWebSetup, getCachedWebSetup } from "../fetchWebSetup"; -import { firebaseApiOrigin } from "../api"; -import { FirebaseError } from "../error"; +import { configstore } from "./configstore"; +import { fetchWebSetup, getCachedWebSetup } from "./fetchWebSetup"; +import { firebaseApiOrigin } from "./api"; +import { FirebaseError } from "./error"; describe("fetchWebSetup module", () => { + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + afterEach(() => { expect(nock.isDone()).to.be.true; }); @@ -26,7 +34,7 @@ describe("fetchWebSetup module", () => { it("should fetch the web app config", async () => { const projectId = "foo"; - nock(firebaseApiOrigin) + nock(firebaseApiOrigin()) .get(`/v1beta1/projects/${projectId}/webApps/-/config`) .reply(200, { some: "config" }); @@ -37,7 +45,7 @@ describe("fetchWebSetup module", () => { it("should store the fetched config", async () => { const projectId = "projectId"; - nock(firebaseApiOrigin) + nock(firebaseApiOrigin()) .get(`/v1beta1/projects/${projectId}/webApps/-/config`) .reply(200, { projectId, some: "config" }); @@ -54,13 +62,13 @@ describe("fetchWebSetup module", () => { it("should throw an error if the request fails", async () => { const projectId = "foo"; - nock(firebaseApiOrigin) + nock(firebaseApiOrigin()) .get(`/v1beta1/projects/${projectId}/webApps/-/config`) .reply(404, { error: "Not Found" }); await expect(fetchWebSetup({ project: projectId })).to.eventually.be.rejectedWith( FirebaseError, - "Not Found" + "Not Found", ); }); diff --git a/src/fetchWebSetup.ts b/src/fetchWebSetup.ts index 7cb69d0408b..82bb8c21526 100644 --- a/src/fetchWebSetup.ts +++ b/src/fetchWebSetup.ts @@ -33,9 +33,9 @@ interface ListSitesResponse { nextPageToken: string; } -const apiClient = new Client({ urlPrefix: firebaseApiOrigin, auth: true, apiVersion: "v1beta1" }); +const apiClient = new Client({ urlPrefix: firebaseApiOrigin(), auth: true, apiVersion: "v1beta1" }); const hostingApiClient = new Client({ - urlPrefix: hostingApiOrigin, + urlPrefix: hostingApiOrigin(), auth: true, apiVersion: "v1beta1", }); @@ -80,7 +80,7 @@ async function listAllSites(projectId: string, nextPageToken?: string): Promise< /** * Construct a fake configuration based on the project ID. */ -function constructDefaultWebSetup(projectId: string): WebConfig { +export function constructDefaultWebSetup(projectId: string): WebConfig { return { projectId, databaseURL: `https://${projectId}.firebaseio.com`, @@ -111,7 +111,7 @@ export async function fetchWebSetup(options: any): Promise { if (defaultSite && defaultSite.appId) { hostingAppId = defaultSite.appId; } - } catch (e) { + } catch (e: any) { logger.debug("Failed to list hosting sites"); logger.debug(e); } diff --git a/src/test/filterTargets.spec.ts b/src/filterTargets.spec.ts similarity index 91% rename from src/test/filterTargets.spec.ts rename to src/filterTargets.spec.ts index 3ef5a160e7e..43e1316977d 100644 --- a/src/test/filterTargets.spec.ts +++ b/src/filterTargets.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; -import { filterTargets } from "../filterTargets"; -import { Options } from "../options"; -import { RC } from "../rc"; +import { filterTargets } from "./filterTargets"; +import { Options } from "./options"; +import { RC } from "./rc"; const SAMPLE_OPTIONS: Options = { cwd: "/", diff --git a/src/filterTargets.ts b/src/filterTargets.ts index 91165414f8a..6593968f27e 100644 --- a/src/filterTargets.ts +++ b/src/filterTargets.ts @@ -17,7 +17,7 @@ export function filterTargets(options: Options, validTargets: string[]): string[ targets, options.only.split(",").map((opt: string) => { return opt.split(":")[0]; - }) + }), ); } else if (options.except) { targets = difference(targets, options.except.split(",")); diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index 4ced73e39c2..02e77aefcf6 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -5,15 +5,21 @@ // 'npm run generate:json-schema' to regenerate the schema files. // -// Sourced from - https://docs.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest -type RequireAtLeastOne = { +import type { HttpsOptions } from "firebase-functions/v2/https"; +import { IngressSetting, MemoryOption, VpcEgressSetting } from "firebase-functions/v2/options"; +import { Runtime, DecommissionedRuntime } from "./deploy/functions/runtimes/supported/types"; + +/** + * Creates a type that requires at least one key to be present in an interface + * type. For example, RequireAtLeastOne<{ foo: string; bar: string }> can hold + * a value of { foo: "a" }, { bar: "b" }, or { foo: "a", bar: "b" } but not {} + * Sourced from - https://docs.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest + */ +export type RequireAtLeastOne = { [K in keyof T]-?: Required> & Partial>>; }[keyof T]; -// should be sourced from - https://github.com/firebase/firebase-tools/blob/master/src/deploy/functions/runtimes/index.ts#L15 -type CloudFunctionRuntimes = "nodejs10" | "nodejs12" | "nodejs14" | "nodejs16"; - -type Deployable = { +export type Deployable = { predeploy?: string | string[]; postdeploy?: string | string[]; }; @@ -30,37 +36,88 @@ type DatabaseMultiple = ({ }> & Deployable)[]; -type HostingSource = { source: string } | { regex: string }; +type FirestoreSingle = { + database?: string; + rules?: string; + indexes?: string; +} & Deployable; -type HostingRedirects = HostingSource & { +type FirestoreMultiple = ({ + rules?: string; + indexes?: string; +} & RequireAtLeastOne<{ + database: string; + target: string; +}> & + Deployable)[]; + +export type HostingSource = { glob: string } | { source: string } | { regex: string }; + +export type HostingRedirects = HostingSource & { destination: string; - type: number; + type?: number; }; -type HostingRewrites = HostingSource & +export type DestinationRewrite = { destination: string }; +export type LegacyFunctionsRewrite = { function: string; region?: string }; +export type FunctionsRewrite = { + function: { + functionId: string; + region?: string; + pinTag?: boolean; + }; +}; +export type RunRewrite = { + run: { + serviceId: string; + region?: string; + pinTag?: boolean; + }; +}; +export type DynamicLinksRewrite = { dynamicLinks: boolean }; +export type HostingRewrites = HostingSource & ( - | { destination: string } - | { function: string } - | { - run: { - serviceId: string; - region?: string; - }; - } - | { dynamicLinks: boolean } + | DestinationRewrite + | LegacyFunctionsRewrite + | FunctionsRewrite + | RunRewrite + | DynamicLinksRewrite ); -type HostingHeaders = HostingSource & { +export type HostingHeaders = HostingSource & { headers: { key: string; value: string; }[]; }; -type HostingBase = { +// Allow only serializable options, since this is in firebase.json +// TODO(jamesdaniels) look into allowing serialized CEL expressions, params, and regexp +// and if we can build this interface automatically via Typescript silliness +interface FrameworksBackendOptions extends HttpsOptions { + omit?: boolean; + cors?: string | boolean; + memory?: MemoryOption; + timeoutSeconds?: number; + minInstances?: number; + maxInstances?: number; + concurrency?: number; + vpcConnector?: string; + vpcConnectorEgressSettings?: VpcEgressSetting; + serviceAccount?: string; + ingressSettings?: IngressSetting; + secrets?: string[]; + // Only allow a single region to be specified + region?: string; + // Invoker can only be public + invoker?: "public"; +} + +export type HostingBase = { public?: string; + source?: string; ignore?: string[]; - appAssociation?: string; + appAssociation?: "AUTO" | "NONE"; cleanUrls?: boolean; trailingSlash?: boolean; redirects?: HostingRedirects[]; @@ -69,14 +126,22 @@ type HostingBase = { i18n?: { root: string; }; + frameworksBackend?: FrameworksBackendOptions; }; -type HostingSingle = HostingBase & { +export type HostingSingle = HostingBase & { site?: string; target?: string; } & Deployable; -type HostingMultiple = (HostingBase & +// N.B. You would expect that a HostingMultiple is a HostingSingle[], but not +// quite. When you only have one hosting object you can omit both `site` and +// `target` because the default site will be looked up and provided for you. +// When you have a list of hosting targets, though, we require all configs +// to specify which site is being targeted. +// If you can assume we've resolved targets, you probably want to use +// HostingResolved, which says you must have site and may have target. +export type HostingMultiple = (HostingBase & RequireAtLeastOne<{ site: string; target: string; @@ -97,18 +162,17 @@ type StorageMultiple = ({ // Full Configs export type DatabaseConfig = DatabaseSingle | DatabaseMultiple; -export type FirestoreConfig = { - rules?: string; - indexes?: string; -} & Deployable; +export type FirestoreConfig = FirestoreSingle | FirestoreMultiple; -export type FunctionsConfig = { - // TODO: Add types for "backend" +export type FunctionConfig = { source?: string; ignore?: string[]; - runtime?: CloudFunctionRuntimes; + runtime?: Exclude; + codebase?: string; } & Deployable; +export type FunctionsConfig = FunctionConfig | FunctionConfig[]; + export type HostingConfig = HostingSingle | HostingMultiple; export type StorageConfig = StorageSingle | StorageMultiple; @@ -129,6 +193,7 @@ export type EmulatorsConfig = { firestore?: { host?: string; port?: number; + websocketPort?: number; }; functions?: { host?: string; @@ -159,11 +224,34 @@ export type EmulatorsConfig = { host?: string; port?: number | string; }; + extensions?: {}; + eventarc?: { + host?: string; + port?: number; + }; + singleProjectMode?: boolean; + dataconnect?: { + host?: string; + port?: number; + }; }; export type ExtensionsConfig = Record; +export type DataConnectSingle = { + // The directory containing dataconnect.yaml for this service + source: string; +} & Deployable; + +export type DataConnectMultiple = DataConnectSingle[]; + +export type DataConnectConfig = DataConnectSingle | DataConnectMultiple; + export type FirebaseConfig = { + /** + * @TJS-format uri + */ + $schema?: string; database?: DatabaseConfig; firestore?: FirestoreConfig; functions?: FunctionsConfig; @@ -172,4 +260,5 @@ export type FirebaseConfig = { remoteconfig?: RemoteConfigConfig; emulators?: EmulatorsConfig; extensions?: ExtensionsConfig; + dataconnect?: DataConnectConfig; }; diff --git a/src/test/firebaseConfigValidate.spec.ts b/src/firebaseConfigValidate.spec.ts similarity index 96% rename from src/test/firebaseConfigValidate.spec.ts rename to src/firebaseConfigValidate.spec.ts index 66ba6bffe74..a1191d7eeed 100644 --- a/src/test/firebaseConfigValidate.spec.ts +++ b/src/firebaseConfigValidate.spec.ts @@ -1,7 +1,6 @@ import { expect } from "chai"; -import { getValidator } from "../firebaseConfigValidate"; -import { FirebaseConfig } from "../firebaseConfig"; -import { valid } from "semver"; +import { getValidator } from "./firebaseConfigValidate"; +import { FirebaseConfig } from "./firebaseConfig"; describe("firebaseConfigValidate", () => { it("should accept a basic, valid config", () => { diff --git a/src/firebaseConfigValidate.ts b/src/firebaseConfigValidate.ts index a2ed3291e24..4a6a7413413 100644 --- a/src/firebaseConfigValidate.ts +++ b/src/firebaseConfigValidate.ts @@ -18,7 +18,7 @@ export function getValidator(): ValidateFunction { if (!_VALIDATOR) { const schemaStr = fs.readFileSync( path.resolve(__dirname, "../schema/firebase-config.json"), - "UTF-8" + "utf-8", ); const schema = JSON.parse(schemaStr); @@ -31,7 +31,7 @@ export function getValidator(): ValidateFunction { export function getErrorMessage(e: ErrorObject) { if (e.keyword === "additionalProperties") { return `Object "${e.dataPath}" in "firebase.json" has unknown property: ${JSON.stringify( - e.params + e.params, )}`; } else if (e.keyword === "required") { return `Object "${ diff --git a/src/firestore/README.md b/src/firestore/README.md index 882c08504ab..21416f4b552 100644 --- a/src/firestore/README.md +++ b/src/firestore/README.md @@ -64,8 +64,47 @@ Note that Cloud Firestore document fields can only be indexed in one [mode](http ```javascript collectionGroup: string // Labeled "Collection ID" in the Firebase console fieldPath: string + ttl?: boolean // Set specified field to have TTL policy and be eligible for deletion indexes: array // Set empty array to disable indexes on this collectionGroup + fieldPath queryScope: string // One of "COLLECTION", "COLLECTION_GROUP" order?: string // One of "ASCENDING", "DESCENDING"; excludes arrayConfig property arrayConfig?: string // If this parameter used, must be "CONTAINS"; excludes order property ``` + +#### TTL Policy + +A TTL policy can be enabled or disabled using the `fieldOverrides` array as it follows: + +```javascript +// Optional, disable index single-field collection group indexes +fieldOverrides: [ + { + collectionGroup: "posts", + fieldPath: "ttlField", + ttl: "true", // Explicitly enable TTL on this Field. + // Disable indexing so empty the indexes array + indexes: [], + }, +]; +``` + +To keep the default indexing in the field and enable a TTL policy: + +```javascript +{ + "fieldOverrides": [ + { + "collectionGroup": "yourCollectionGroup", + "fieldPath": "yourFieldPath", + "ttl": true, + "indexes": [ + { "order": "ASCENDING", "queryScope": "COLLECTION_GROUP" }, + { "order": "DESCENDING", "queryScope": "COLLECTION_GROUP" }, + { "arrayConfig": "CONTAINS", "queryScope": "COLLECTION_GROUP" } + ] + } + ] +} +``` + +For more information about time-to-live (TTL) policies review the [official documentation](https://cloud.google.com/firestore/docs/ttl). diff --git a/src/firestore/api-sort.test.ts b/src/firestore/api-sort.test.ts new file mode 100644 index 00000000000..c11da278a35 --- /dev/null +++ b/src/firestore/api-sort.test.ts @@ -0,0 +1,78 @@ +import { expect } from "chai"; + +import { Backup, BackupSchedule, DayOfWeek } from "../gcp/firestore"; +import { durationFromSeconds } from "../gcp/proto"; +import * as sort from "./api-sort"; + +describe("compareApiBackup", () => { + it("should compare backups by location", () => { + const nam5Backup: Backup = { + name: "projects/example/locations/nam5/backups/backupid", + }; + const usWest1Backup: Backup = { + name: "projects/example/locations/us-west1/backups/backupid", + }; + expect(sort.compareApiBackup(usWest1Backup, nam5Backup)).to.greaterThanOrEqual(1); + expect(sort.compareApiBackup(nam5Backup, usWest1Backup)).to.lessThanOrEqual(-1); + }); + + it("should compare backups by snapshotTime (descending) if location is the same", () => { + const earlierBackup: Backup = { + name: "projects/example/locations/nam5/backups/backupid", + snapshotTime: "2024-01-01T00:00:00.000000Z", + }; + const laterBackup: Backup = { + name: "projects/example/locations/nam5/backups/backupid", + snapshotTime: "2024-02-02T00:00:00.000000Z", + }; + expect(sort.compareApiBackup(earlierBackup, laterBackup)).to.greaterThanOrEqual(1); + expect(sort.compareApiBackup(laterBackup, earlierBackup)).to.lessThanOrEqual(-1); + }); + + it("should compare backups by full name if location and snapshotTime are the same", () => { + const nam5Backup1: Backup = { + name: "projects/example/locations/nam5/backups/earlier-backupid", + snapshotTime: "2024-01-01T00:00:00.000000Z", + }; + const nam5Backup2: Backup = { + name: "projects/example/locations/nam5/backups/later-backupid", + snapshotTime: "2024-01-01T00:00:00.000000Z", + }; + expect(sort.compareApiBackup(nam5Backup2, nam5Backup1)).to.greaterThanOrEqual(1); + expect(sort.compareApiBackup(nam5Backup1, nam5Backup2)).to.lessThanOrEqual(-1); + }); +}); + +describe("compareApiBackupSchedule", () => { + it("daily schedules should precede weekly ones", () => { + const dailySchedule: BackupSchedule = { + name: "projects/example/databases/mydatabase/backupSchedules/schedule", + dailyRecurrence: {}, + retention: durationFromSeconds(60 * 60 * 24), + }; + const weeklySchedule: BackupSchedule = { + name: "projects/example/databases/mydatabase/backupSchedules/schedule", + weeklyRecurrence: { + day: DayOfWeek.FRIDAY, + }, + retention: durationFromSeconds(60 * 60 * 24 * 7), + }; + expect(sort.compareApiBackupSchedule(weeklySchedule, dailySchedule)).to.greaterThanOrEqual(1); + expect(sort.compareApiBackup(dailySchedule, weeklySchedule)).to.lessThanOrEqual(-1); + }); + + it("should compare schedules with the same recurrence by name", () => { + const dailySchedule1: BackupSchedule = { + name: "projects/example/databases/mydatabase/backupSchedules/schedule1", + dailyRecurrence: {}, + retention: durationFromSeconds(60 * 60 * 24), + }; + const dailySchedule2: BackupSchedule = { + name: "projects/example/databases/mydatabase/backupSchedules/schedule2", + dailyRecurrence: {}, + retention: durationFromSeconds(60 * 60 * 24), + }; + expect(sort.compareApiBackupSchedule(dailySchedule1, dailySchedule2)).to.lessThanOrEqual(-1); + expect(sort.compareApiBackup(dailySchedule2, dailySchedule1)).to.greaterThanOrEqual(1); + }); +}); diff --git a/src/firestore/api-sort.ts b/src/firestore/api-sort.ts new file mode 100644 index 00000000000..3991a831749 --- /dev/null +++ b/src/firestore/api-sort.ts @@ -0,0 +1,277 @@ +import * as API from "./api-types"; +import * as Spec from "./api-spec"; +import * as util from "./util"; +import { Backup, BackupSchedule } from "../gcp/firestore"; + +const QUERY_SCOPE_SEQUENCE = [ + API.QueryScope.COLLECTION_GROUP, + API.QueryScope.COLLECTION, + undefined, +]; + +const ORDER_SEQUENCE = [API.Order.ASCENDING, API.Order.DESCENDING, undefined]; + +const ARRAY_CONFIG_SEQUENCE = [API.ArrayConfig.CONTAINS, undefined]; + +/** + * Compare two Index spec entries for sorting. + * + * Comparisons: + * 1) The collection group. + * 2) The query scope. + * 3) The fields list. + */ +export function compareSpecIndex(a: Spec.Index, b: Spec.Index): number { + if (a.collectionGroup !== b.collectionGroup) { + return a.collectionGroup.localeCompare(b.collectionGroup); + } + + if (a.queryScope !== b.queryScope) { + return compareQueryScope(a.queryScope, b.queryScope); + } + + return compareArrays(a.fields, b.fields, compareIndexField); +} + +/** + * Compare two Index api entries for sorting. + * + * Comparisons: + * 1) The collection group. + * 2) The query scope. + * 3) The fields list. + */ +export function compareApiIndex(a: API.Index, b: API.Index): number { + // When these indexes are used as part of a field override, the name is + // not always present or relevant. + if (a.name && b.name) { + const aName = util.parseIndexName(a.name); + const bName = util.parseIndexName(b.name); + + if (aName.collectionGroupId !== bName.collectionGroupId) { + return aName.collectionGroupId.localeCompare(bName.collectionGroupId); + } + } + + if (a.queryScope !== b.queryScope) { + return compareQueryScope(a.queryScope, b.queryScope); + } + + return compareArrays(a.fields, b.fields, compareIndexField); +} + +/** + * Compare two Database api entries for sorting. + * + * Comparisons: + * 1) The databaseId (name) + */ +export function compareApiDatabase(a: API.DatabaseResp, b: API.DatabaseResp): number { + // Name should always be unique and present + return a.name > b.name ? 1 : -1; +} + +/** + * Compare two Location api entries for sorting. + * + * Comparisons: + * 1) The locationId. + */ +export function compareLocation(a: API.Location, b: API.Location): number { + // LocationId should always be unique and present + return a.locationId > b.locationId ? 1 : -1; +} + +/** + * Compare two Backup API entries for sorting. + * Ordered by: location, snapshotTime (descending), then name + */ +export function compareApiBackup(a: Backup, b: Backup): number { + // the location is embedded in the name (projects/myproject/locations/mylocation/backups/mybackup) + const aLocation = a.name!.split("/")[3]; + const bLocation = b.name!.split("/")[3]; + if (aLocation && bLocation && aLocation !== bLocation) { + return aLocation > bLocation ? 1 : -1; + } + + if (a.snapshotTime && b.snapshotTime && a.snapshotTime !== b.snapshotTime) { + return a.snapshotTime > b.snapshotTime ? -1 : 1; + } + + // Name should always be unique and present + return a.name! > b.name! ? 1 : -1; +} + +/** + * Compare two BackupSchedule API entries for sorting. + * + * Daily schedules should precede weekly ones. Break ties by name. + */ +export function compareApiBackupSchedule(a: BackupSchedule, b: BackupSchedule): number { + if (a.dailyRecurrence && !b.dailyRecurrence) { + return -1; + } else if (a.weeklyRecurrence && b.dailyRecurrence) { + return 1; + } + + // Name should always be unique and present + return a.name! > b.name! ? 1 : -1; +} + +/** + * Compare two Field api entries for sorting. + * + * Comparisons: + * 1) The collection group. + * 2) The field path. + * 3) The indexes list in the config. + */ +export function compareApiField(a: API.Field, b: API.Field): number { + const aName = util.parseFieldName(a.name); + const bName = util.parseFieldName(b.name); + + if (aName.collectionGroupId !== bName.collectionGroupId) { + return aName.collectionGroupId.localeCompare(bName.collectionGroupId); + } + + if (aName.fieldPath !== bName.fieldPath) { + return aName.fieldPath.localeCompare(bName.fieldPath); + } + + return compareArraysSorted( + a.indexConfig.indexes || [], + b.indexConfig.indexes || [], + compareApiIndex, + ); +} + +/** + * Compare two Field override specs for sorting. + * + * Comparisons: + * 1) The collection group. + * 2) The field path. + * 3) The ttl. + * 3) The list of indexes. + */ +export function compareFieldOverride(a: Spec.FieldOverride, b: Spec.FieldOverride): number { + if (a.collectionGroup !== b.collectionGroup) { + return a.collectionGroup.localeCompare(b.collectionGroup); + } + + // The ttl override can be undefined, we only guarantee that true values will + // come last since those overrides should be executed after disabling TTL per collection. + const compareTtl = Number(!!a.ttl) - Number(!!b.ttl); + if (compareTtl) { + return compareTtl; + } + + if (a.fieldPath !== b.fieldPath) { + return a.fieldPath.localeCompare(b.fieldPath); + } + + return compareArraysSorted(a.indexes, b.indexes, compareFieldIndex); +} + +/** + * Compare two IndexField objects. + * + * Comparisons: + * 1) Field path. + * 2) Sort order (if it exists). + * 3) Array config (if it exists). + * 4) Vector config (if it exists). + */ +function compareIndexField(a: API.IndexField, b: API.IndexField): number { + if (a.fieldPath !== b.fieldPath) { + return a.fieldPath.localeCompare(b.fieldPath); + } + + if (a.order !== b.order) { + return compareOrder(a.order, b.order); + } + + if (a.arrayConfig !== b.arrayConfig) { + return compareArrayConfig(a.arrayConfig, b.arrayConfig); + } + + if (a.vectorConfig !== b.vectorConfig) { + return compareVectorConfig(a.vectorConfig, b.vectorConfig); + } + + return 0; +} + +function compareFieldIndex(a: Spec.FieldIndex, b: Spec.FieldIndex): number { + if (a.queryScope !== b.queryScope) { + return compareQueryScope(a.queryScope, b.queryScope); + } + + if (a.order !== b.order) { + return compareOrder(a.order, b.order); + } + + if (a.arrayConfig !== b.arrayConfig) { + return compareArrayConfig(a.arrayConfig, b.arrayConfig); + } + + return 0; +} + +function compareQueryScope(a: API.QueryScope, b: API.QueryScope): number { + return QUERY_SCOPE_SEQUENCE.indexOf(a) - QUERY_SCOPE_SEQUENCE.indexOf(b); +} + +function compareOrder(a?: API.Order, b?: API.Order): number { + return ORDER_SEQUENCE.indexOf(a) - ORDER_SEQUENCE.indexOf(b); +} + +function compareArrayConfig(a?: API.ArrayConfig, b?: API.ArrayConfig): number { + return ARRAY_CONFIG_SEQUENCE.indexOf(a) - ARRAY_CONFIG_SEQUENCE.indexOf(b); +} + +function compareVectorConfig(a?: API.VectorConfig, b?: API.VectorConfig): number { + if (!a) { + if (!b) { + return 0; + } else { + return 1; + } + } else if (!b) { + return -1; + } + return a.dimension - b.dimension; +} + +/** + * Compare two arrays of objects by looking for the first + * non-equal element and comparing them. + * + * If the shorter array is a perfect prefix of the longer array, + * then the shorter array is sorted first. + */ +function compareArrays(a: T[], b: T[], fn: (x: T, y: T) => number): number { + const minFields = Math.min(a.length, b.length); + for (let i = 0; i < minFields; i++) { + const cmp = fn(a[i], b[i]); + if (cmp !== 0) { + return cmp; + } + } + + return a.length - b.length; +} + +/** + * Compare two arrays of objects by first sorting each array, then + * looking for the first non-equal element and comparing them. + * + * If the shorter array is a perfect prefix of the longer array, + * then the shorter array is sorted first. + */ +function compareArraysSorted(a: T[], b: T[], fn: (x: T, y: T) => number): number { + const aSorted = a.sort(fn); + const bSorted = b.sort(fn); + + return compareArrays(aSorted, bSorted, fn); +} diff --git a/src/firestore/api-spec.ts b/src/firestore/api-spec.ts new file mode 100644 index 00000000000..da491b8438a --- /dev/null +++ b/src/firestore/api-spec.ts @@ -0,0 +1,43 @@ +/** + * NOTE: + * Changes to this source file will likely affect the Firebase documentation. + * Please review and update the README as needed and notify firebase-docs@google.com. + */ + +import * as API from "./api-types"; + +/** + * An entry specifying a compound or other non-default index. + */ +export interface Index { + collectionGroup: string; + queryScope: API.QueryScope; + fields: API.IndexField[]; +} + +/** + * An entry specifying field index configuration override. + */ +export interface FieldOverride { + collectionGroup: string; + fieldPath: string; + ttl?: boolean; + indexes: FieldIndex[]; +} + +/** + * Entry specifying a single-field index. + */ +export interface FieldIndex { + queryScope: API.QueryScope; + order?: API.Order; + arrayConfig?: API.ArrayConfig; +} + +/** + * Specification for the JSON file that is used for index deployment, + */ +export interface IndexFile { + indexes: Index[]; + fieldOverrides?: FieldOverride[]; +} diff --git a/src/firestore/api-types.ts b/src/firestore/api-types.ts new file mode 100644 index 00000000000..9e564947923 --- /dev/null +++ b/src/firestore/api-types.ts @@ -0,0 +1,156 @@ +/** + * The v1beta1 indexes API used a 'mode' field to represent the indexing mode. + * This information has now been split into the fields 'arrayConfig' and 'order'. + * We allow use of 'mode' (for now) so that the move to v1beta2/v1 is not + * breaking when we can understand the developer's intent. + */ +export enum Mode { + ASCENDING = "ASCENDING", + DESCENDING = "DESCENDING", + ARRAY_CONTAINS = "ARRAY_CONTAINS", +} + +export enum QueryScope { + COLLECTION = "COLLECTION", + COLLECTION_GROUP = "COLLECTION_GROUP", +} + +export enum Order { + ASCENDING = "ASCENDING", + DESCENDING = "DESCENDING", +} + +export enum ArrayConfig { + CONTAINS = "CONTAINS", +} + +export interface VectorConfig { + dimension: number; + flat?: {}; +} + +export enum State { + CREATING = "CREATING", + READY = "READY", + NEEDS_REPAIR = "NEEDS_REPAIR", +} + +export enum StateTtl { + CREATING = "CREATING", + ACTIVE = "ACTIVE", + NEEDS_REPAIR = "NEEDS_REPAIR", +} + +/** + * An Index as it is represented in the Firestore v1beta2 indexes API. + */ +export interface Index { + name?: string; + queryScope: QueryScope; + fields: IndexField[]; + state?: State; +} + +/** + * A field in an index. + */ +export interface IndexField { + fieldPath: string; + order?: Order; + arrayConfig?: ArrayConfig; + vectorConfig?: VectorConfig; +} + +/** + * TTL policy configuration for a field + */ +export interface TtlConfig { + state: StateTtl; +} + +/** + * Represents a single field in the database. + * + * If a field has an empty indexConfig, that means all + * default indexes are exempted. + */ +export interface Field { + name: string; + indexConfig: IndexConfig; + ttlConfig?: TtlConfig; +} + +/** + * Index configuration overrides for a field. + */ +export interface IndexConfig { + ancestorField?: string; + indexes?: Index[]; +} + +export interface Location { + name: string; + labels: any; + metadata: any; + locationId: string; + displayName: string; +} + +export enum DatabaseType { + DATASTORE_MODE = "DATASTORE_MODE", + FIRESTORE_NATIVE = "FIRESTORE_NATIVE", +} + +export enum DatabaseDeleteProtectionStateOption { + ENABLED = "ENABLED", + DISABLED = "DISABLED", +} + +export enum DatabaseDeleteProtectionState { + ENABLED = "DELETE_PROTECTION_ENABLED", + DISABLED = "DELETE_PROTECTION_DISABLED", +} + +export enum PointInTimeRecoveryEnablementOption { + ENABLED = "ENABLED", + DISABLED = "DISABLED", +} + +export enum PointInTimeRecoveryEnablement { + ENABLED = "POINT_IN_TIME_RECOVERY_ENABLED", + DISABLED = "POINT_IN_TIME_RECOVERY_DISABLED", +} + +export interface DatabaseReq { + locationId?: string; + type?: DatabaseType; + deleteProtectionState?: DatabaseDeleteProtectionState; + pointInTimeRecoveryEnablement?: PointInTimeRecoveryEnablement; +} + +export interface DatabaseResp { + name: string; + uid: string; + createTime: string; + updateTime: string; + locationId: string; + type: DatabaseType; + concurrencyMode: string; + appEngineIntegrationMode: string; + keyPrefix: string; + deleteProtectionState: DatabaseDeleteProtectionState; + pointInTimeRecoveryEnablement: PointInTimeRecoveryEnablement; + etag: string; + versionRetentionPeriod: string; + earliestVersionTime: string; +} + +export interface RestoreDatabaseReq { + databaseId: string; + backup: string; +} + +export enum RecurrenceType { + DAILY = "DAILY", + WEEKLY = "WEEKLY", +} diff --git a/src/firestore/api.ts b/src/firestore/api.ts new file mode 100644 index 00000000000..1e1351add19 --- /dev/null +++ b/src/firestore/api.ts @@ -0,0 +1,732 @@ +import * as clc from "colorette"; + +import { logger } from "../logger"; +import * as utils from "../utils"; +import * as validator from "./validator"; + +import * as types from "./api-types"; +import * as Spec from "./api-spec"; +import * as sort from "./api-sort"; +import * as util from "./util"; +import { confirm } from "../prompt"; +import { firestoreOrigin } from "../api"; +import { FirebaseError } from "../error"; +import { Client } from "../apiv2"; +import { PrettyPrint } from "./pretty-print"; + +export class FirestoreApi { + apiClient = new Client({ urlPrefix: firestoreOrigin(), apiVersion: "v1" }); + printer = new PrettyPrint(); + + /** + * Deploy an index specification to the specified project. + * @param options the CLI options. + * @param indexes an array of objects, each will be validated and then converted + * to an {@link Spec.Index}. + * @param fieldOverrides an array of objects, each will be validated and then + * converted to an {@link Spec.FieldOverride}. + */ + async deploy( + options: { project: string; nonInteractive: boolean; force: boolean }, + indexes: any[], + fieldOverrides: any[], + databaseId = "(default)", + ): Promise { + const spec = this.upgradeOldSpec({ + indexes, + fieldOverrides, + }); + + this.validateSpec(spec); + + // Now that the spec is validated we can safely assert these types. + const indexesToDeploy: Spec.Index[] = spec.indexes; + const fieldOverridesToDeploy: Spec.FieldOverride[] = spec.fieldOverrides; + + const existingIndexes: types.Index[] = await this.listIndexes(options.project, databaseId); + const existingFieldOverrides: types.Field[] = await this.listFieldOverrides( + options.project, + databaseId, + ); + + const indexesToDelete = existingIndexes.filter((index) => { + return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec)); + }); + + // We only want to delete fields where there is nothing in the local file with the same + // (collectionGroup, fieldPath) pair. Otherwise any differences will be resolved + // as part of the "PATCH" process. + const fieldOverridesToDelete = existingFieldOverrides.filter((field) => { + return !fieldOverridesToDeploy.some((spec) => { + const parsedName = util.parseFieldName(field.name); + + if (parsedName.collectionGroupId !== spec.collectionGroup) { + return false; + } + + if (parsedName.fieldPath !== spec.fieldPath) { + return false; + } + + return true; + }); + }); + + let shouldDeleteIndexes = options.force; + if (indexesToDelete.length > 0) { + if (options.nonInteractive && !options.force) { + utils.logLabeledBullet( + "firestore", + `there are ${indexesToDelete.length} indexes defined in your project that are not present in your ` + + "firestore indexes file. To delete them, run this command with the --force flag.", + ); + } else if (!options.force) { + const indexesString = indexesToDelete + .map((x) => this.printer.prettyIndexString(x, false)) + .join("\n\t"); + utils.logLabeledBullet( + "firestore", + `The following indexes are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}`, + ); + } + + if (!shouldDeleteIndexes) { + shouldDeleteIndexes = await confirm({ + nonInteractive: options.nonInteractive, + force: options.force, + default: false, + message: + "Would you like to delete these indexes? Selecting no will continue the rest of the deployment.", + }); + } + } + + for (const index of indexesToDeploy) { + const exists = existingIndexes.some((x) => this.indexMatchesSpec(x, index)); + if (exists) { + logger.debug(`Skipping existing index: ${JSON.stringify(index)}`); + } else { + logger.debug(`Creating new index: ${JSON.stringify(index)}`); + await this.createIndex(options.project, index, databaseId); + } + } + + if (shouldDeleteIndexes && indexesToDelete.length > 0) { + utils.logLabeledBullet("firestore", `Deleting ${indexesToDelete.length} indexes...`); + for (const index of indexesToDelete) { + await this.deleteIndex(index); + } + } + + let shouldDeleteFields = options.force; + if (fieldOverridesToDelete.length > 0) { + if (options.nonInteractive && !options.force) { + utils.logLabeledBullet( + "firestore", + `there are ${fieldOverridesToDelete.length} field overrides defined in your project that are not present in your ` + + "firestore indexes file. To delete them, run this command with the --force flag.", + ); + } else if (!options.force) { + const indexesString = fieldOverridesToDelete + .map((x) => this.printer.prettyFieldString(x)) + .join("\n\t"); + utils.logLabeledBullet( + "firestore", + `The following field overrides are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}`, + ); + } + + if (!shouldDeleteFields) { + shouldDeleteFields = await confirm({ + nonInteractive: options.nonInteractive, + force: options.force, + default: false, + message: + "Would you like to delete these field overrides? Selecting no will continue the rest of the deployment.", + }); + } + } + + // Disabling TTL must be executed first in case another field is enabled for + // the same collection in the same deployment. + const sortedFieldOverridesToDeploy = fieldOverridesToDeploy.sort(sort.compareFieldOverride); + for (const field of sortedFieldOverridesToDeploy) { + const exists = existingFieldOverrides.some((x) => this.fieldMatchesSpec(x, field)); + if (exists) { + logger.debug(`Skipping existing field override: ${JSON.stringify(field)}`); + } else { + logger.debug(`Updating field override: ${JSON.stringify(field)}`); + await this.patchField(options.project, field, databaseId); + } + } + + if (shouldDeleteFields && fieldOverridesToDelete.length > 0) { + utils.logLabeledBullet( + "firestore", + `Deleting ${fieldOverridesToDelete.length} field overrides...`, + ); + for (const field of fieldOverridesToDelete) { + await this.deleteField(field); + } + } + } + + /** + * List all indexes that exist on a given project. + * @param project the Firebase project id. + */ + async listIndexes(project: string, databaseId = "(default)"): Promise { + const url = `/projects/${project}/databases/${databaseId}/collectionGroups/-/indexes`; + const res = await this.apiClient.get<{ indexes?: types.Index[] }>(url); + const indexes = res.body.indexes; + if (!indexes) { + return []; + } + + return indexes.map((index: any): types.Index => { + // Ignore any fields that point at the document ID, as those are implied + // in all indexes. + const fields = index.fields.filter((field: types.IndexField) => { + return field.fieldPath !== "__name__"; + }); + + return { + name: index.name, + state: index.state, + queryScope: index.queryScope, + fields, + }; + }); + } + + /** + * List all field configuration overrides defined on the given project. + * @param project the Firebase project. + */ + async listFieldOverrides(project: string, databaseId = "(default)"): Promise { + const parent = `projects/${project}/databases/${databaseId}/collectionGroups/-`; + const url = `/${parent}/fields?filter=indexConfig.usesAncestorConfig=false OR ttlConfig:*`; + + const res = await this.apiClient.get<{ fields?: types.Field[] }>(url); + const fields = res.body.fields; + + // This should never be the case, since the API always returns the __default__ + // configuration, but this is a defensive check. + if (!fields) { + return []; + } + + // Ignore the default config, only list other fields. + return fields.filter((field) => { + return !field.name.includes("__default__"); + }); + } + + /** + * Turn an array of indexes and field overrides into a {@link Spec.IndexFile} suitable for use + * in an indexes.json file. + */ + makeIndexSpec(indexes: types.Index[], fields?: types.Field[]): Spec.IndexFile { + const indexesJson = indexes.map((index) => { + return { + collectionGroup: util.parseIndexName(index.name).collectionGroupId, + queryScope: index.queryScope, + fields: index.fields, + }; + }); + + if (!fields) { + logger.debug("No field overrides specified, using []."); + fields = []; + } + + const fieldsJson = fields.map((field) => { + const parsedName = util.parseFieldName(field.name); + const fieldIndexes = field.indexConfig.indexes || []; + return { + collectionGroup: parsedName.collectionGroupId, + fieldPath: parsedName.fieldPath, + ttl: !!field.ttlConfig, + + indexes: fieldIndexes.map((index) => { + const firstField = index.fields[0]; + return { + order: firstField.order, + arrayConfig: firstField.arrayConfig, + queryScope: index.queryScope, + }; + }), + }; + }); + + const sortedIndexes = indexesJson.sort(sort.compareSpecIndex); + const sortedFields = fieldsJson.sort(sort.compareFieldOverride); + return { + indexes: sortedIndexes, + fieldOverrides: sortedFields, + }; + } + + /** + * Validate that an object is a valid index specification. + * @param spec the object, normally parsed from JSON. + */ + validateSpec(spec: any): void { + validator.assertHas(spec, "indexes"); + + spec.indexes.forEach((index: any) => { + this.validateIndex(index); + }); + + if (spec.fieldOverrides) { + spec.fieldOverrides.forEach((field: any) => { + this.validateField(field); + }); + } + } + + /** + * Validate that an arbitrary object is safe to use as an {@link types.Field}. + */ + validateIndex(index: any): void { + validator.assertHas(index, "collectionGroup"); + validator.assertHas(index, "queryScope"); + validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope)); + + validator.assertHas(index, "fields"); + + index.fields.forEach((field: any) => { + validator.assertHas(field, "fieldPath"); + validator.assertHasOneOf(field, ["order", "arrayConfig", "vectorConfig"]); + + if (field.order) { + validator.assertEnum(field, "order", Object.keys(types.Order)); + } + + if (field.arrayConfig) { + validator.assertEnum(field, "arrayConfig", Object.keys(types.ArrayConfig)); + } + + if (field.vectorConfig) { + validator.assertType("vectorConfig.dimension", field.vectorConfig.dimension, "number"); + validator.assertHas(field.vectorConfig, "flat"); + } + }); + } + + /** + * Validate that an arbitrary object is safe to use as an {@link Spec.FieldOverride}. + * @param field + */ + validateField(field: any): void { + validator.assertHas(field, "collectionGroup"); + validator.assertHas(field, "fieldPath"); + validator.assertHas(field, "indexes"); + + if (typeof field.ttl !== "undefined") { + validator.assertType("ttl", field.ttl, "boolean"); + } + + field.indexes.forEach((index: any) => { + validator.assertHasOneOf(index, ["arrayConfig", "order"]); + + if (index.arrayConfig) { + validator.assertEnum(index, "arrayConfig", Object.keys(types.ArrayConfig)); + } + + if (index.order) { + validator.assertEnum(index, "order", Object.keys(types.Order)); + } + + if (index.queryScope) { + validator.assertEnum(index, "queryScope", Object.keys(types.QueryScope)); + } + }); + } + + /** + * Update the configuration of a field. Note that this kicks off a long-running + * operation for index creation/deletion so the update is complete when this + * method returns. + * @param project the Firebase project. + * @param spec the new field override specification. + */ + async patchField( + project: string, + spec: Spec.FieldOverride, + databaseId = "(default)", + ): Promise { + const url = `/projects/${project}/databases/${databaseId}/collectionGroups/${spec.collectionGroup}/fields/${spec.fieldPath}`; + + const indexes = spec.indexes.map((index) => { + return { + queryScope: index.queryScope, + fields: [ + { + fieldPath: spec.fieldPath, + arrayConfig: index.arrayConfig, + order: index.order, + }, + ], + }; + }); + + let data = { + indexConfig: { + indexes, + }, + }; + + if (spec.ttl) { + data = Object.assign(data, { + ttlConfig: {}, + }); + } + + if (typeof spec.ttl !== "undefined") { + await this.apiClient.patch(url, data); + } else { + await this.apiClient.patch(url, data, { queryParams: { updateMask: "indexConfig" } }); + } + } + + /** + * Delete an existing field overrides on the specified project. + */ + deleteField(field: types.Field): Promise { + const url = field.name; + const data = {}; + + return this.apiClient.patch(`/${url}`, data); + } + + /** + * Create a new index on the specified project. + */ + createIndex(project: string, index: Spec.Index, databaseId = "(default)"): Promise { + const url = `/projects/${project}/databases/${databaseId}/collectionGroups/${index.collectionGroup}/indexes`; + return this.apiClient.post(url, { + fields: index.fields, + queryScope: index.queryScope, + }); + } + + /** + * Delete an existing index on the specified project. + */ + deleteIndex(index: types.Index): Promise { + const url = index.name!; + return this.apiClient.delete(`/${url}`); + } + + /** + * Determine if an API Index and a Spec Index are functionally equivalent. + */ + indexMatchesSpec(index: types.Index, spec: Spec.Index): boolean { + const collection = util.parseIndexName(index.name).collectionGroupId; + if (collection !== spec.collectionGroup) { + return false; + } + + if (index.queryScope !== spec.queryScope) { + return false; + } + + if (index.fields.length !== spec.fields.length) { + return false; + } + + let i = 0; + while (i < index.fields.length) { + const iField = index.fields[i]; + const sField = spec.fields[i]; + + if (iField.fieldPath !== sField.fieldPath) { + return false; + } + + if (iField.order !== sField.order) { + return false; + } + + if (iField.arrayConfig !== sField.arrayConfig) { + return false; + } + + i++; + } + + return true; + } + + /** + * Determine if an API Field and a Spec Field are functionally equivalent. + */ + fieldMatchesSpec(field: types.Field, spec: Spec.FieldOverride): boolean { + const parsedName = util.parseFieldName(field.name); + + if (parsedName.collectionGroupId !== spec.collectionGroup) { + return false; + } + + if (parsedName.fieldPath !== spec.fieldPath) { + return false; + } + + if (typeof spec.ttl !== "undefined" && util.booleanXOR(!!field.ttlConfig, spec.ttl)) { + return false; + } else if (!!field.ttlConfig && typeof spec.ttl === "undefined") { + utils.logLabeledBullet( + "firestore", + `there are TTL field overrides for collection ${spec.collectionGroup} defined in your project that are not present in your ` + + "firestore indexes file. The TTL policy won't be deleted since is not specified as false.", + ); + } + + const fieldIndexes = field.indexConfig.indexes || []; + if (fieldIndexes.length !== spec.indexes.length) { + return false; + } + + const fieldModes = fieldIndexes.map((index) => { + const firstField = index.fields[0]; + return firstField.order || firstField.arrayConfig; + }); + + const specModes = spec.indexes.map((index) => { + return index.order || index.arrayConfig; + }); + + // Confirms that the two objects have the same set of enabled indexes without + // caring about specification order. + for (const mode of fieldModes) { + if (!specModes.includes(mode)) { + return false; + } + } + + return true; + } + + /** + * Take a object that may represent an old v1beta1 indexes spec + * and convert it to the new v1/v1 spec format. + * + * This function is meant to be run **before** validation and + * works on a purely best-effort basis. + */ + upgradeOldSpec(spec: any): any { + const result = { + indexes: [], + fieldOverrides: spec.fieldOverrides || [], + }; + + if (!(spec.indexes && spec.indexes.length > 0)) { + return result; + } + + // Try to detect use of the old API, warn the users. + if (spec.indexes[0].collectionId) { + utils.logBullet( + clc.bold(clc.cyan("firestore:")) + + " your indexes indexes are specified in the v1beta1 API format. " + + "Please upgrade to the new index API format by running " + + clc.bold("firebase firestore:indexes") + + " again and saving the result.", + ); + } + + result.indexes = spec.indexes.map((index: any) => { + const i = { + collectionGroup: index.collectionGroup || index.collectionId, + queryScope: index.queryScope || types.QueryScope.COLLECTION, + fields: [], + }; + + if (index.fields) { + i.fields = index.fields.map((field: any) => { + const f: any = { + fieldPath: field.fieldPath, + }; + + if (field.order) { + f.order = field.order; + } else if (field.arrayConfig) { + f.arrayConfig = field.arrayConfig; + } else if (field.vectorConfig) { + f.vectorConfig = field.vectorConfig; + } else if (field.mode === types.Mode.ARRAY_CONTAINS) { + f.arrayConfig = types.ArrayConfig.CONTAINS; + } else { + f.order = field.mode; + } + + return f; + }); + } + + return i; + }); + + return result; + } + + /** + * List all databases that exist on a given project. + * @param project the Firebase project id. + */ + async listDatabases(project: string): Promise { + const url = `/projects/${project}/databases`; + const res = await this.apiClient.get<{ databases?: types.DatabaseResp[] }>(url); + const databases = res.body.databases; + if (!databases) { + return []; + } + + return databases; + } + + /** + * List all locations that exist on a given project. + * @param project the Firebase project id. + */ + async locations(project: string): Promise { + const url = `/projects/${project}/locations`; + const res = await this.apiClient.get<{ locations?: types.Location[] }>(url); + const locations = res.body.locations; + if (!locations) { + return []; + } + + return locations; + } + + /** + * Get info on a Firestore database. + * @param project the Firebase project id. + * @param databaseId the id of the Firestore Database + */ + async getDatabase(project: string, databaseId: string): Promise { + const url = `/projects/${project}/databases/${databaseId}`; + const res = await this.apiClient.get(url); + const database = res.body; + if (!database) { + throw new FirebaseError("Not found"); + } + + return database; + } + + /** + * Create a named Firestore Database + * @param project the Firebase project id. + * @param databaseId the name of the Firestore Database + * @param locationId the id of the region the database will be created in + * @param type FIRESTORE_NATIVE or DATASTORE_MODE + * @param deleteProtectionState DELETE_PROTECTION_ENABLED or DELETE_PROTECTION_DISABLED + * @param pointInTimeRecoveryEnablement POINT_IN_TIME_RECOVERY_ENABLED or POINT_IN_TIME_RECOVERY_DISABLED + */ + async createDatabase( + project: string, + databaseId: string, + locationId: string, + type: types.DatabaseType, + deleteProtectionState: types.DatabaseDeleteProtectionState, + pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement, + ): Promise { + const url = `/projects/${project}/databases`; + const payload: types.DatabaseReq = { + type, + locationId, + deleteProtectionState, + pointInTimeRecoveryEnablement, + }; + const options = { queryParams: { databaseId: databaseId } }; + const res = await this.apiClient.post( + url, + payload, + options, + ); + const database = res.body.response; + if (!database) { + throw new FirebaseError("Not found"); + } + + return database; + } + + /** + * Update a named Firestore Database + * @param project the Firebase project id. + * @param databaseId the name of the Firestore Database + * @param deleteProtectionState DELETE_PROTECTION_ENABLED or DELETE_PROTECTION_DISABLED + * @param pointInTimeRecoveryEnablement POINT_IN_TIME_RECOVERY_ENABLED or POINT_IN_TIME_RECOVERY_DISABLED + */ + async updateDatabase( + project: string, + databaseId: string, + deleteProtectionState?: types.DatabaseDeleteProtectionState, + pointInTimeRecoveryEnablement?: types.PointInTimeRecoveryEnablement, + ): Promise { + const url = `/projects/${project}/databases/${databaseId}`; + const payload: types.DatabaseReq = { + deleteProtectionState, + pointInTimeRecoveryEnablement, + }; + const res = await this.apiClient.patch( + url, + payload, + ); + const database = res.body.response; + if (!database) { + throw new FirebaseError("Not found"); + } + + return database; + } + + /** + * Delete a Firestore Database + * @param project the Firebase project id. + * @param databaseId the name of the Firestore Database + */ + async deleteDatabase(project: string, databaseId: string): Promise { + const url = `/projects/${project}/databases/${databaseId}`; + const res = await this.apiClient.delete<{ response?: types.DatabaseResp }>(url); + const database = res.body.response; + if (!database) { + throw new FirebaseError("Not found"); + } + + return database; + } + + /** + * Restore a Firestore Database from a backup. + * @param project the Firebase project id. + * @param databaseId the ID of the Firestore Database to be restored into + * @param backupName Name of the backup from which to restore + */ + async restoreDatabase( + project: string, + databaseId: string, + backupName: string, + ): Promise { + const url = `/projects/${project}/databases:restore`; + const payload: types.RestoreDatabaseReq = { + databaseId, + backup: backupName, + }; + const options = { queryParams: { databaseId: databaseId } }; + const res = await this.apiClient.post< + types.RestoreDatabaseReq, + { response?: types.DatabaseResp } + >(url, payload, options); + const database = res.body.response; + if (!database) { + throw new FirebaseError("Not found"); + } + + return database; + } +} diff --git a/src/firestore/backupUtils.test.ts b/src/firestore/backupUtils.test.ts new file mode 100644 index 00000000000..de338efd402 --- /dev/null +++ b/src/firestore/backupUtils.test.ts @@ -0,0 +1,21 @@ +import { expect } from "chai"; + +import { calculateRetention } from "./backupUtils"; + +describe("calculateRetention", () => { + it("should accept minutes", () => { + expect(calculateRetention("5m")).to.eq(300); + }); + + it("should accept hours", () => { + expect(calculateRetention("3h")).to.eq(10800); + }); + + it("should accept days", () => { + expect(calculateRetention("2d")).to.eq(172800); + }); + + it("should accept weeks", () => { + expect(calculateRetention("3w")).to.eq(1814400); + }); +}); diff --git a/src/firestore/backupUtils.ts b/src/firestore/backupUtils.ts new file mode 100644 index 00000000000..4f0200945ac --- /dev/null +++ b/src/firestore/backupUtils.ts @@ -0,0 +1,42 @@ +import { FirebaseError } from "../error"; +import { FirestoreOptions } from "./options"; + +/** + * A regex to test for valid duration strings. + */ +export const DURATION_REGEX = /^(\d+)([hdmw])$/; + +/** + * Basic durations in seconds. + */ +export enum Duration { + MINUTE = 60, + HOUR = 60 * 60, + DAY = 24 * 60 * 60, + WEEK = 7 * 24 * 60 * 60, +} + +const DURATIONS: { [d: string]: Duration } = { + m: Duration.MINUTE, + h: Duration.HOUR, + d: Duration.DAY, + w: Duration.WEEK, +}; + +/** + * calculateRetention returns the duration in seconds from the provided flag. + * @param flag string duration (e.g. "1d"). + * @return a duration in seconds. + */ +export function calculateRetention(flag: NonNullable): number { + const match = DURATION_REGEX.exec(flag); + if (!match) { + throw new FirebaseError(`"retention" flag must be a duration string (e.g. 24h, 2w, or 7d)`); + } + const d = parseInt(match[1], 10) * DURATIONS[match[2]]; + if (isNaN(d)) { + throw new FirebaseError(`Failed to parse provided retention time "${flag}"`); + } + + return d; +} diff --git a/src/firestore/checkDatabaseType.ts b/src/firestore/checkDatabaseType.ts index 7fd8c666c5d..db01e803b11 100644 --- a/src/firestore/checkDatabaseType.ts +++ b/src/firestore/checkDatabaseType.ts @@ -1,24 +1,25 @@ -import * as api from "../api"; +import { firestoreOrigin } from "../api"; +import { Client } from "../apiv2"; import { logger } from "../logger"; /** * Determine the Firestore database type for a given project. One of: * - DATABASE_TYPE_UNSPECIFIED (unspecified) - * - CLOUD_DATASTORE (Datastore legacy) - * - CLOUD_FIRESTORE (Firestore native mode) - * - CLOUD_DATASTORE_COMPATIBILITY (Firestore datastore mode) + * - DATASTORE_MODE(Datastore legacy) + * - FIRESTORE_NATIVE (Firestore native mode) * * @param projectId the Firebase project ID. */ -export async function checkDatabaseType(projectId: string): Promise { +export async function checkDatabaseType( + projectId: string, +): Promise<"DATASTORE_MODE" | "FIRESTORE_NATIVE" | "DATABASE_TYPE_UNSPECIFIED" | undefined> { try { - const resp = await api.request("GET", "/v1/apps/" + projectId, { - auth: true, - origin: api.appengineOrigin, - }); - - return resp.body.databaseType; - } catch (err) { + const client = new Client({ urlPrefix: firestoreOrigin(), apiVersion: "v1" }); + const resp = await client.get<{ + type?: "DATASTORE_MODE" | "FIRESTORE_NATIVE" | "DATABASE_TYPE_UNSPECIFIED"; + }>(`/projects/${projectId}/databases/(default)`); + return resp.body.type; + } catch (err: any) { logger.debug("error getting database type", err); return undefined; } diff --git a/src/firestore/delete.ts b/src/firestore/delete.ts index e72f9fdf457..aadaa9584af 100644 --- a/src/firestore/delete.ts +++ b/src/firestore/delete.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as ProgressBar from "progress"; import * as apiv2 from "../apiv2"; @@ -38,6 +38,7 @@ export class FirestoreDelete { private recursive: boolean; private shallow: boolean; private allCollections: boolean; + private databaseId: string; private readBatchSize: number; private maxPendingDeletes: number; @@ -61,13 +62,19 @@ export class FirestoreDelete { constructor( project: string, path: string | undefined, - options: { recursive?: boolean; shallow?: boolean; allCollections?: boolean } + options: { + recursive?: boolean; + shallow?: boolean; + allCollections?: boolean; + databaseId: string; + }, ) { this.project = project; this.path = path || ""; this.recursive = Boolean(options.recursive); this.shallow = Boolean(options.shallow); this.allCollections = Boolean(options.allCollections); + this.databaseId = options.databaseId; // Tunable deletion parameters this.readBatchSize = 7500; @@ -79,7 +86,8 @@ export class FirestoreDelete { this.path = this.path.replace(/(^\/+|\/+$)/g, ""); this.allDescendants = this.recursive; - this.root = "projects/" + project + "/databases/(default)/documents"; + + this.root = `projects/${project}/databases/${this.databaseId}/documents`; const segments = this.path.split("/"); this.isDocumentPath = segments.length % 2 === 0; @@ -105,7 +113,7 @@ export class FirestoreDelete { this.apiClient = new apiv2.Client({ auth: true, apiVersion: "v1", - urlPrefix: firestoreOriginOrEmulator, + urlPrefix: firestoreOriginOrEmulator(), }); } @@ -158,7 +166,7 @@ export class FirestoreDelete { private collectionDescendantsQuery( allDescendants: boolean, batchSize: number, - startAfter?: string + startAfter?: string, ) { const nullChar = String.fromCharCode(0); @@ -277,7 +285,7 @@ export class FirestoreDelete { private getDescendantBatch( allDescendants: boolean, batchSize: number, - startAfter?: string + startAfter?: string, ): Promise { const url = this.parent + ":runQuery"; const body = this.isDocumentPath @@ -386,7 +394,7 @@ export class FirestoreDelete { numPendingDeletes++; firestore - .deleteDocuments(this.project, toDelete) + .deleteDocuments(this.project, toDelete, true) .then((numDeleted) => { FirestoreDelete.progressBar.tick(numDeleted); numDocsDeleted += numDeleted; @@ -411,7 +419,7 @@ export class FirestoreDelete { if (newBatchSize < this.deleteBatchSize) { utils.logLabeledWarning( "firestore", - `delete transaction too large, reducing batch size from ${this.deleteBatchSize} to ${newBatchSize}` + `delete transaction too large, reducing batch size from ${this.deleteBatchSize} to ${newBatchSize}`, ); this.setDeleteBatchSize(newBatchSize); } @@ -446,7 +454,7 @@ export class FirestoreDelete { return false; }; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const intervalId = setInterval(() => { if (queueLoop()) { clearInterval(intervalId); @@ -473,7 +481,7 @@ export class FirestoreDelete { let initialDelete; if (this.isDocumentPath) { const doc = { name: this.root + "/" + this.path }; - initialDelete = firestore.deleteDocument(doc).catch((err) => { + initialDelete = firestore.deleteDocument(doc, true).catch((err) => { logger.debug("deletePath:initialDelete:error", err); if (this.allDescendants) { // On a recursive delete, we are insensitive to @@ -500,7 +508,7 @@ export class FirestoreDelete { */ public deleteDatabase(): Promise { return firestore - .listCollectionIds(this.project) + .listCollectionIds(this.project, true) .catch((err) => { logger.debug("deleteDatabase:listCollectionIds:error", err); return utils.reject("Unable to list collection IDs"); @@ -514,6 +522,7 @@ export class FirestoreDelete { const collectionId = collectionIds[i]; const deleteOp = new FirestoreDelete(this.project, collectionId, { recursive: true, + databaseId: this.databaseId, }); promises.push(deleteOp.execute()); @@ -527,7 +536,7 @@ export class FirestoreDelete { * Check if a path has any children. Useful for determining * if deleting a path will affect more than one document. * - * @return a promise that retruns true if the path has children and false otherwise. + * @return a promise that returns true if the path has children and false otherwise. */ public checkHasChildren(): Promise { return this.getDescendantBatch(true, 1).then((docs) => { @@ -554,4 +563,8 @@ export class FirestoreDelete { return this.deletePath(); }); } + + public getRoot(): string { + return this.root; + } } diff --git a/src/test/firestore/encodeFirestoreValue.spec.ts b/src/firestore/encodeFirestoreValue.spec.ts similarity index 91% rename from src/test/firestore/encodeFirestoreValue.spec.ts rename to src/firestore/encodeFirestoreValue.spec.ts index e8aec03faec..3c04ecffc6c 100644 --- a/src/test/firestore/encodeFirestoreValue.spec.ts +++ b/src/firestore/encodeFirestoreValue.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; -import { FirebaseError } from "../../error"; -import { encodeFirestoreValue } from "../../firestore/encodeFirestoreValue"; +import { FirebaseError } from "../error"; +import { encodeFirestoreValue } from "./encodeFirestoreValue"; describe("encodeFirestoreValue", () => { it("should encode known types", () => { diff --git a/src/firestore/encodeFirestoreValue.ts b/src/firestore/encodeFirestoreValue.ts index 000df07ef54..b9d8e8a0623 100644 --- a/src/firestore/encodeFirestoreValue.ts +++ b/src/firestore/encodeFirestoreValue.ts @@ -11,23 +11,23 @@ function isPlainObject(input: any): boolean { } function encodeHelper(val: any): any { - if (_.isString(val)) { + if (typeof val === "string") { return { stringValue: val }; } - if (_.isBoolean(val)) { + if (val === !!val) { return { booleanValue: val }; } - if (_.isInteger(val)) { + if (Number.isInteger(val)) { return { integerValue: val }; } // Integers are handled above, the remaining numbers are treated as doubles - if (_.isNumber(val)) { + if (typeof val === "number") { return { doubleValue: val }; } - if (_.isDate(val)) { + if (val instanceof Date && !Number.isNaN(val)) { return { timestampValue: val.toISOString() }; } - if (_.isArray(val)) { + if (Array.isArray(val)) { const encodedElements = []; for (const v of val) { const enc = encodeHelper(v); @@ -39,7 +39,7 @@ function encodeHelper(val: any): any { arrayValue: { values: encodedElements }, }; } - if (_.isNull(val)) { + if (val === null) { return { nullValue: "NULL_VALUE" }; } if (val instanceof Buffer || val instanceof Uint8Array) { @@ -52,10 +52,16 @@ function encodeHelper(val: any): any { } throw new FirebaseError( `Cannot encode ${val} to a Firestore Value. ` + - "The emulator does not yet support Firestore document reference values or geo points." + "The emulator does not yet support Firestore document reference values or geo points.", ); } -export function encodeFirestoreValue(data: any): { [key: string]: any } { - return _.mapValues(data, encodeHelper); +export function encodeFirestoreValue(data: any): Record { + return Object.entries(data).reduce( + (acc, [key, val]) => { + acc[key] = encodeHelper(val); + return acc; + }, + {} as Record, + ); } diff --git a/src/firestore/fsConfig.ts b/src/firestore/fsConfig.ts new file mode 100644 index 00000000000..2cc06995942 --- /dev/null +++ b/src/firestore/fsConfig.ts @@ -0,0 +1,87 @@ +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { Options } from "../options"; + +export interface ParsedFirestoreConfig { + database: string; + rules?: string; + indexes?: string; +} + +export function getFirestoreConfig(projectId: string, options: Options): ParsedFirestoreConfig[] { + const fsConfig = options.config.src.firestore; + if (fsConfig === undefined) { + return []; + } + + const rc = options.rc; + let allDatabases = !options.only; + const onlyDatabases = new Set(); + if (options.only) { + const split = options.only.split(","); + if (split.includes("firestore")) { + allDatabases = true; + } else { + for (const value of split) { + if (value.startsWith("firestore:")) { + const target = value.split(":")[1]; + onlyDatabases.add(target); + } + } + } + } + + // single DB + if (!Array.isArray(fsConfig)) { + if (fsConfig) { + // databaseId is (default) if none provided + const databaseId = fsConfig.database || `(default)`; + return [{ rules: fsConfig.rules, indexes: fsConfig.indexes, database: databaseId }]; + } else { + logger.debug("Possibly invalid database config: ", JSON.stringify(fsConfig)); + return []; + } + } + + const results: ParsedFirestoreConfig[] = []; + for (const c of fsConfig) { + const { database, target } = c; + if (target) { + if (allDatabases || onlyDatabases.has(target)) { + // Make sure the target exists (this will throw otherwise) + rc.requireTarget(projectId, "firestore", target); + // Get a list of firestore instances the target maps to + const databases = rc.target(projectId, "firestore", target); + for (const database of databases) { + results.push({ database, rules: c.rules, indexes: c.indexes }); + } + onlyDatabases.delete(target); + } + } else if (database) { + if (allDatabases || onlyDatabases.has(database)) { + results.push(c as ParsedFirestoreConfig); + onlyDatabases.delete(database); + } + } else { + throw new FirebaseError('Must supply either "target" or "databaseId" in firestore config'); + } + } + + // If user specifies firestore:rules or firestore:indexes make sure we don't throw an error if this doesn't match a database name + if (onlyDatabases.has("rules")) { + onlyDatabases.delete("rules"); + } + if (onlyDatabases.has("indexes")) { + onlyDatabases.delete("indexes"); + } + + if (!allDatabases && onlyDatabases.size !== 0) { + throw new FirebaseError( + `Could not find configurations in firebase.json for the following database targets: ${[ + ...onlyDatabases, + ].join(", ")}`, + ); + } + + return results; +} diff --git a/src/firestore/indexes-api.ts b/src/firestore/indexes-api.ts deleted file mode 100644 index 39f3556130e..00000000000 --- a/src/firestore/indexes-api.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * The v1beta1 indexes API used a 'mode' field to represent the indexing mode. - * This information has now been split into the fields 'arrayConfig' and 'order'. - * We allow use of 'mode' (for now) so that the move to v1beta2/v1 is not - * breaking when we can understand the developer's intent. - */ -export enum Mode { - ASCENDING = "ASCENDING", - DESCENDING = "DESCENDING", - ARRAY_CONTAINS = "ARRAY_CONTAINS", -} - -export enum QueryScope { - COLLECTION = "COLLECTION", - COLLECTION_GROUP = "COLLECTION_GROUP", -} - -export enum Order { - ASCENDING = "ASCENDING", - DESCENDING = "DESCENDING", -} - -export enum ArrayConfig { - CONTAINS = "CONTAINS", -} - -export enum State { - CREATING = "CREATING", - READY = "READY", - NEEDS_REPAIR = "NEEDS_REPAIR", -} - -/** - * An Index as it is represented in the Firestore v1beta2 indexes API. - */ -export interface Index { - name?: string; - queryScope: QueryScope; - fields: IndexField[]; - state?: State; -} - -/** - * A field in an index. - */ -export interface IndexField { - fieldPath: string; - order?: Order; - arrayConfig?: ArrayConfig; -} - -/** - * Represents a single field in the database. - * - * If a field has an empty indexConfig, that means all - * default indexes are exempted. - */ -export interface Field { - name: string; - indexConfig: IndexConfig; -} - -/** - * Index configuration overrides for a field. - */ -export interface IndexConfig { - ancestorField?: string; - indexes?: Index[]; -} diff --git a/src/firestore/indexes-sort.ts b/src/firestore/indexes-sort.ts deleted file mode 100644 index be2179bbe3a..00000000000 --- a/src/firestore/indexes-sort.ts +++ /dev/null @@ -1,192 +0,0 @@ -import * as API from "./indexes-api"; -import * as Spec from "./indexes-spec"; -import * as util from "./util"; - -const QUERY_SCOPE_SEQUENCE = [ - API.QueryScope.COLLECTION_GROUP, - API.QueryScope.COLLECTION, - undefined, -]; - -const ORDER_SEQUENCE = [API.Order.ASCENDING, API.Order.DESCENDING, undefined]; - -const ARRAY_CONFIG_SEQUENCE = [API.ArrayConfig.CONTAINS, undefined]; - -/** - * Compare two Index spec entries for sorting. - * - * Comparisons: - * 1) The collection group. - * 2) The query scope. - * 3) The fields list. - */ -export function compareSpecIndex(a: Spec.Index, b: Spec.Index): number { - if (a.collectionGroup !== b.collectionGroup) { - return a.collectionGroup.localeCompare(b.collectionGroup); - } - - if (a.queryScope !== b.queryScope) { - return compareQueryScope(a.queryScope, b.queryScope); - } - - return compareArrays(a.fields, b.fields, compareIndexField); -} - -/** - * Compare two Index api entries for sorting. - * - * Comparisons: - * 1) The collection group. - * 2) The query scope. - * 3) The fields list. - */ -export function compareApiIndex(a: API.Index, b: API.Index): number { - // When these indexes are used as part of a field override, the name is - // not always present or relevant. - if (a.name && b.name) { - const aName = util.parseIndexName(a.name); - const bName = util.parseIndexName(b.name); - - if (aName.collectionGroupId !== bName.collectionGroupId) { - return aName.collectionGroupId.localeCompare(bName.collectionGroupId); - } - } - - if (a.queryScope !== b.queryScope) { - return compareQueryScope(a.queryScope, b.queryScope); - } - - return compareArrays(a.fields, b.fields, compareIndexField); -} - -/** - * Compare two Field api entries for sorting. - * - * Comparisons: - * 1) The collection group. - * 2) The field path. - * 3) The indexes list in the config. - */ -export function compareApiField(a: API.Field, b: API.Field): number { - const aName = util.parseFieldName(a.name); - const bName = util.parseFieldName(b.name); - - if (aName.collectionGroupId !== bName.collectionGroupId) { - return aName.collectionGroupId.localeCompare(bName.collectionGroupId); - } - - if (aName.fieldPath !== bName.fieldPath) { - return aName.fieldPath.localeCompare(bName.fieldPath); - } - - return compareArraysSorted( - a.indexConfig.indexes || [], - b.indexConfig.indexes || [], - compareApiIndex - ); -} - -/** - * Compare two Field override specs for sorting. - * - * Comparisons: - * 1) The collection group. - * 2) The field path. - * 3) The list of indexes. - */ -export function compareFieldOverride(a: Spec.FieldOverride, b: Spec.FieldOverride): number { - if (a.collectionGroup !== b.collectionGroup) { - return a.collectionGroup.localeCompare(b.collectionGroup); - } - - if (a.fieldPath !== b.fieldPath) { - return a.fieldPath.localeCompare(b.fieldPath); - } - - return compareArraysSorted(a.indexes, b.indexes, compareFieldIndex); -} - -/** - * Compare two IndexField objects. - * - * Comparisons: - * 1) Field path. - * 2) Sort order (if it exists). - * 3) Array config (if it exists). - */ -function compareIndexField(a: API.IndexField, b: API.IndexField): number { - if (a.fieldPath !== b.fieldPath) { - return a.fieldPath.localeCompare(b.fieldPath); - } - - if (a.order !== b.order) { - return compareOrder(a.order, b.order); - } - - if (a.arrayConfig !== b.arrayConfig) { - return compareArrayConfig(a.arrayConfig, b.arrayConfig); - } - - return 0; -} - -function compareFieldIndex(a: Spec.FieldIndex, b: Spec.FieldIndex): number { - if (a.queryScope !== b.queryScope) { - return compareQueryScope(a.queryScope, b.queryScope); - } - - if (a.order !== b.order) { - return compareOrder(a.order, b.order); - } - - if (a.arrayConfig !== b.arrayConfig) { - return compareArrayConfig(a.arrayConfig, b.arrayConfig); - } - - return 0; -} - -function compareQueryScope(a: API.QueryScope, b: API.QueryScope): number { - return QUERY_SCOPE_SEQUENCE.indexOf(a) - QUERY_SCOPE_SEQUENCE.indexOf(b); -} - -function compareOrder(a?: API.Order, b?: API.Order): number { - return ORDER_SEQUENCE.indexOf(a) - ORDER_SEQUENCE.indexOf(b); -} - -function compareArrayConfig(a?: API.ArrayConfig, b?: API.ArrayConfig): number { - return ARRAY_CONFIG_SEQUENCE.indexOf(a) - ARRAY_CONFIG_SEQUENCE.indexOf(b); -} - -/** - * Compare two arrays of objects by looking for the first - * non-equal element and comparing them. - * - * If the shorter array is a perfect prefix of the longer array, - * then the shorter array is sorted first. - */ -function compareArrays(a: T[], b: T[], fn: (x: T, y: T) => number): number { - const minFields = Math.min(a.length, b.length); - for (let i = 0; i < minFields; i++) { - const cmp = fn(a[i], b[i]); - if (cmp !== 0) { - return cmp; - } - } - - return a.length - b.length; -} - -/** - * Compare two arrays of objects by first sorting each array, then - * looking for the first non-equal element and comparing them. - * - * If the shorter array is a perfect prefix of the longer array, - * then the shorter array is sorted first. - */ -function compareArraysSorted(a: T[], b: T[], fn: (x: T, y: T) => number): number { - const aSorted = a.sort(fn); - const bSorted = b.sort(fn); - - return compareArrays(aSorted, bSorted, fn); -} diff --git a/src/firestore/indexes-spec.ts b/src/firestore/indexes-spec.ts deleted file mode 100644 index a85ea6ee31b..00000000000 --- a/src/firestore/indexes-spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * NOTE: - * Changes to this source file will likely affect the Firebase documentation. - * Please review and update the README as needed and notify firebase-docs@google.com. - */ - -import * as API from "./indexes-api"; - -/** - * An entry specifying a compound or other non-default index. - */ -export interface Index { - collectionGroup: string; - queryScope: API.QueryScope; - fields: API.IndexField[]; -} - -/** - * An entry specifying field index configuration override. - */ -export interface FieldOverride { - collectionGroup: string; - fieldPath: string; - indexes: FieldIndex[]; -} - -/** - * Entry specifying a single-field index. - */ -export interface FieldIndex { - queryScope: API.QueryScope; - order?: API.Order; - arrayConfig?: API.ArrayConfig; -} - -/** - * Specification for the JSON file that is used for index deployment, - */ -export interface IndexFile { - indexes: Index[]; - fieldOverrides?: FieldOverride[]; -} diff --git a/src/firestore/indexes.spec.ts b/src/firestore/indexes.spec.ts new file mode 100644 index 00000000000..7e4f99661b8 --- /dev/null +++ b/src/firestore/indexes.spec.ts @@ -0,0 +1,727 @@ +import { expect } from "chai"; +import { FirestoreApi } from "./api"; +import { FirebaseError } from "../error"; +import * as API from "./api-types"; +import * as Spec from "./api-spec"; +import * as sort from "./api-sort"; + +const idx = new FirestoreApi(); + +const VALID_SPEC = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", order: "DESCENDING" }, + { fieldPath: "baz", arrayConfig: "CONTAINS" }, + ], + }, + ], + fieldOverrides: [ + { + collectionGroup: "collection", + fieldPath: "foo", + indexes: [ + { order: "ASCENDING", scope: "COLLECTION" }, + { arrayConfig: "CONTAINS", scope: "COLLECTION" }, + ], + }, + ], +}; + +describe("IndexValidation", () => { + it("should accept a valid v1beta2 index spec", () => { + idx.validateSpec(VALID_SPEC); + }); + + it("should not change a valid v1beta2 index spec after upgrade", () => { + const upgraded = idx.upgradeOldSpec(VALID_SPEC); + expect(upgraded).to.eql(VALID_SPEC); + }); + + it("should accept an empty spec", () => { + const empty = { + indexes: [], + }; + + idx.validateSpec(idx.upgradeOldSpec(empty)); + }); + + it("should accept a valid v1beta1 index spec after upgrade", () => { + idx.validateSpec( + idx.upgradeOldSpec({ + indexes: [ + { + collectionId: "collection", + fields: [ + { fieldPath: "foo", mode: "ASCENDING" }, + { fieldPath: "bar", mode: "DESCENDING" }, + { fieldPath: "baz", mode: "ARRAY_CONTAINS" }, + ], + }, + ], + }), + ); + }); + + it("should accept a valid vectorConfig index", () => { + idx.validateSpec( + idx.upgradeOldSpec({ + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { + fieldPath: "embedding", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + }, + ], + }), + ); + }); + + it("should accept a valid vectorConfig index after upgrade", () => { + idx.validateSpec({ + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { + fieldPath: "embedding", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + }, + ], + }); + }); + + it("should accept a valid vectorConfig index with another field", () => { + idx.validateSpec({ + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { + fieldPath: "embedding", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + }, + ], + }); + }); + + it("should reject invalid vectorConfig dimension", () => { + expect(() => { + idx.validateSpec({ + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { + fieldPath: "embedding", + vectorConfig: { + dimension: "wrongType", + flat: {}, + }, + }, + ], + }, + ], + }); + }).to.throw(FirebaseError, /Property "vectorConfig.dimension" must be of type number/); + }); + + it("should reject invalid vectorConfig missing flat type", () => { + expect(() => { + idx.validateSpec({ + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { + fieldPath: "embedding", + vectorConfig: { + dimension: 100, + }, + }, + ], + }, + ], + }); + }).to.throw(FirebaseError, /Must contain "flat"/); + }); + + it("should reject an incomplete index spec", () => { + expect(() => { + idx.validateSpec({ + indexes: [ + { + collectionGroup: "collection", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", order: "DESCENDING" }, + ], + }, + ], + }); + }).to.throw(FirebaseError, /Must contain "queryScope"/); + }); + + it("should reject an overspecified index spec", () => { + expect(() => { + idx.validateSpec({ + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING", arrayConfig: "CONTAINES" }, + { fieldPath: "bar", order: "DESCENDING" }, + ], + }, + ], + }); + }).to.throw(FirebaseError, /Must contain exactly one of "order,arrayConfig,vectorConfig"/); + }); +}); +describe("IndexSpecMatching", () => { + it("should identify a positive index spec match", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", arrayConfig: API.ArrayConfig.CONTAINS }, + ], + state: API.State.READY, + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", arrayConfig: "CONTAINS" }, + ], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(true); + }); + + it("should identify a negative index spec match", () => { + const apiIndex = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "DESCENDING" }, + { fieldPath: "bar", arrayConfig: "CONTAINS" }, + ], + state: API.State.READY, + } as API.Index; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", arrayConfig: "CONTAINS" }, + ], + } as Spec.Index; + + // The second spec contains ASCENDING where the former contains DESCENDING + expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); + }); + + it("should identify a positive field spec match", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", order: "ASCENDING" }], + }, + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }], + }, + ], + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "abc123", + indexes: [ + { order: "ASCENDING", queryScope: "COLLECTION" }, + { arrayConfig: "CONTAINS", queryScope: "COLLECTION" }, + ], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + + it("should identify a positive field spec match with ttl specified as false", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", order: "ASCENDING" }], + }, + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }], + }, + ], + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "abc123", + ttl: false, + indexes: [ + { order: "ASCENDING", queryScope: "COLLECTION" }, + { arrayConfig: "CONTAINS", queryScope: "COLLECTION" }, + ], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + + it("should identify a positive ttl field spec match", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "fieldTtl", order: "ASCENDING" }], + }, + ], + }, + ttlConfig: { + state: "ACTIVE", + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "fieldTtl", + ttl: true, + indexes: [{ order: "ASCENDING", queryScope: "COLLECTION" }], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + + it("should identify a negative ttl field spec match", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "fieldTtl", order: "ASCENDING" }], + }, + ], + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "fieldTtl", + ttl: true, + indexes: [{ order: "ASCENDING", queryScope: "COLLECTION" }], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false); + }); + + it("should match a field spec with all indexes excluded", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", + indexConfig: {}, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "abc123", + indexes: [], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + + it("should match a field spec with only ttl", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/ttlField", + ttlConfig: { + state: "ACTIVE", + }, + indexConfig: {}, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "ttlField", + ttl: true, + indexes: [], + } as Spec.FieldOverride; + + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); + }); + + it("should identify a negative field spec match", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", + indexConfig: { + indexes: [ + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", order: "ASCENDING" }], + }, + { + queryScope: "COLLECTION", + fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }], + }, + ], + }, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "abc123", + indexes: [ + { order: "DESCENDING", queryScope: "COLLECTION" }, + { arrayConfig: "CONTAINS", queryScope: "COLLECTION" }, + ], + } as Spec.FieldOverride; + + // The second spec contains "DESCENDING" where the first contains "ASCENDING" + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false); + }); + + it("should identify a negative field spec match with ttl as false", () => { + const apiField = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl", + ttlConfig: { + state: "ACTIVE", + }, + indexConfig: {}, + } as API.Field; + + const specField = { + collectionGroup: "collection", + fieldPath: "fieldTtl", + ttl: false, + indexes: [], + } as Spec.FieldOverride; + + // The second spec contains "false" for ttl where the first contains "true" + // for ttl + expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false); + }); +}); + +describe("IndexSorting", () => { + it("should be able to handle empty arrays", () => { + expect(([] as Spec.Index[]).sort(sort.compareSpecIndex)).to.eql([]); + expect(([] as Spec.FieldOverride[]).sort(sort.compareFieldOverride)).to.eql([]); + expect(([] as API.Index[]).sort(sort.compareApiIndex)).to.eql([]); + expect(([] as API.Field[]).sort(sort.compareApiField)).to.eql([]); + }); + + it("should correctly sort an array of Spec indexes", () => { + // Sorts first because of collectionGroup + const a: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + }; + + // fieldA ASCENDING should sort before fieldA DESCENDING + const b: Spec.Index = { + collectionGroup: "collectionB", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + order: API.Order.ASCENDING, + }, + ], + }; + + // This compound index sorts before the following simple + // index because the first element sorts first. + const c: Spec.Index = { + collectionGroup: "collectionB", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + order: API.Order.ASCENDING, + }, + { + fieldPath: "fieldB", + order: API.Order.ASCENDING, + }, + ], + }; + + const d: Spec.Index = { + collectionGroup: "collectionB", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldB", + order: API.Order.ASCENDING, + }, + ], + }; + + const e: Spec.Index = { + collectionGroup: "collectionB", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldB", + order: API.Order.ASCENDING, + }, + { + fieldPath: "fieldA", + order: API.Order.ASCENDING, + }, + ], + }; + + expect([b, a, e, d, c].sort(sort.compareSpecIndex)).to.eql([a, b, c, d, e]); + }); + + it("should correcty sort an array of Spec field overrides", () => { + // Sorts first because of collectionGroup + const a: Spec.FieldOverride = { + collectionGroup: "collectionA", + fieldPath: "fieldA", + indexes: [], + }; + + const b: Spec.FieldOverride = { + collectionGroup: "collectionB", + fieldPath: "fieldA", + indexes: [], + }; + + // Order indexes sort before Array indexes + const c: Spec.FieldOverride = { + collectionGroup: "collectionB", + fieldPath: "fieldB", + indexes: [ + { + queryScope: API.QueryScope.COLLECTION, + order: API.Order.ASCENDING, + }, + ], + }; + + const d: Spec.FieldOverride = { + collectionGroup: "collectionB", + fieldPath: "fieldB", + indexes: [ + { + queryScope: API.QueryScope.COLLECTION, + arrayConfig: API.ArrayConfig.CONTAINS, + }, + ], + }; + + expect([b, a, d, c].sort(sort.compareFieldOverride)).to.eql([a, b, c, d]); + }); + + it("should sort ttl true to be last in an array of Spec field overrides", () => { + // Sorts first because of collectionGroup + const a: Spec.FieldOverride = { + collectionGroup: "collectionA", + fieldPath: "fieldA", + ttl: false, + indexes: [], + }; + const b: Spec.FieldOverride = { + collectionGroup: "collectionA", + fieldPath: "fieldB", + ttl: true, + indexes: [], + }; + const c: Spec.FieldOverride = { + collectionGroup: "collectionB", + fieldPath: "fieldA", + ttl: false, + indexes: [], + }; + const d: Spec.FieldOverride = { + collectionGroup: "collectionB", + fieldPath: "fieldB", + ttl: true, + indexes: [], + }; + expect([b, a, d, c].sort(sort.compareFieldOverride)).to.eql([a, b, c, d]); + }); + + it("should correctly sort an array of API indexes", () => { + // Sorts first because of collectionGroup + const a: API.Index = { + name: "/projects/project/databases/(default)/collectionGroups/collectionA/indexes/a", + queryScope: API.QueryScope.COLLECTION, + fields: [], + }; + + // fieldA ASCENDING should sort before fieldA DESCENDING + const b: API.Index = { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/b", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + order: API.Order.ASCENDING, + }, + ], + }; + + // This compound index sorts before the following simple + // index because the first element sorts first. + const c: API.Index = { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/c", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + order: API.Order.ASCENDING, + }, + { + fieldPath: "fieldB", + order: API.Order.ASCENDING, + }, + ], + }; + + const d: API.Index = { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/d", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + order: API.Order.DESCENDING, + }, + ], + }; + + const e: API.Index = { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/e", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + vectorConfig: { + dimension: 100, + flat: {}, + }, + }, + ], + }; + + const f: API.Index = { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/f", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + vectorConfig: { + dimension: 200, + flat: {}, + }, + }, + ], + }; + + // This Index is invalid, but is used to verify sort ordering on undefined + // fields. + const g: API.Index = { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/g", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { + fieldPath: "fieldA", + }, + ], + }; + + expect([b, a, d, g, f, e, c].sort(sort.compareApiIndex)).to.eql([a, b, c, d, e, f, g]); + }); + + it("should correctly sort an array of API field overrides", () => { + // Sorts first because of collectionGroup + const a: API.Field = { + name: "/projects/myproject/databases/(default)/collectionGroups/collectionA/fields/fieldA", + indexConfig: { + indexes: [], + }, + }; + + const b: API.Field = { + name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldA", + indexConfig: { + indexes: [], + }, + }; + + // Order indexes sort before Array indexes + const c: API.Field = { + name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldB", + indexConfig: { + indexes: [ + { + queryScope: API.QueryScope.COLLECTION, + fields: [{ fieldPath: "fieldB", order: API.Order.DESCENDING }], + }, + ], + }, + }; + + const d: API.Field = { + name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldB", + indexConfig: { + indexes: [ + { + queryScope: API.QueryScope.COLLECTION, + fields: [{ fieldPath: "fieldB", arrayConfig: API.ArrayConfig.CONTAINS }], + }, + ], + }, + }; + + expect([b, a, d, c].sort(sort.compareApiField)).to.eql([a, b, c, d]); + }); +}); diff --git a/src/firestore/indexes.ts b/src/firestore/indexes.ts deleted file mode 100644 index 9068391aa1c..00000000000 --- a/src/firestore/indexes.ts +++ /dev/null @@ -1,648 +0,0 @@ -import * as clc from "cli-color"; - -import * as api from "../api"; -import { logger } from "../logger"; -import * as utils from "../utils"; -import * as validator from "./validator"; - -import * as API from "./indexes-api"; -import * as Spec from "./indexes-spec"; -import * as sort from "./indexes-sort"; -import * as util from "./util"; -import { promptOnce } from "../prompt"; - -export class FirestoreIndexes { - /** - * Deploy an index specification to the specified project. - * @param options the CLI options. - * @param indexes an array of objects, each will be validated and then converted - * to an {@link Spec.Index}. - * @param fieldOverrides an array of objects, each will be validated and then - * converted to an {@link Spec.FieldOverride}. - */ - async deploy( - options: { project: string; nonInteractive: boolean; force: boolean }, - indexes: any[], - fieldOverrides: any[] - ): Promise { - const spec = this.upgradeOldSpec({ - indexes, - fieldOverrides, - }); - - this.validateSpec(spec); - - // Now that the spec is validated we can safely assert these types. - const indexesToDeploy: Spec.Index[] = spec.indexes; - const fieldOverridesToDeploy: Spec.FieldOverride[] = spec.fieldOverrides; - - const existingIndexes: API.Index[] = await this.listIndexes(options.project); - const existingFieldOverrides: API.Field[] = await this.listFieldOverrides(options.project); - - const indexesToDelete = existingIndexes.filter((index) => { - return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec)); - }); - - // We only want to delete fields where there is nothing in the local file with the same - // (collectionGroup, fieldPath) pair. Otherwise any differences will be resolved - // as part of the "PATCH" process. - const fieldOverridesToDelete = existingFieldOverrides.filter((field) => { - return !fieldOverridesToDeploy.some((spec) => { - const parsedName = util.parseFieldName(field.name); - - if (parsedName.collectionGroupId !== spec.collectionGroup) { - return false; - } - - if (parsedName.fieldPath !== spec.fieldPath) { - return false; - } - - return true; - }); - }); - - let shouldDeleteIndexes = options.force; - if (indexesToDelete.length > 0) { - if (options.nonInteractive && !options.force) { - utils.logLabeledBullet( - "firestore", - `there are ${indexesToDelete.length} indexes defined in your project that are not present in your ` + - "firestore indexes file. To delete them, run this command with the --force flag." - ); - } else if (!options.force) { - const indexesString = indexesToDelete - .map((x) => this.prettyIndexString(x, false)) - .join("\n\t"); - utils.logLabeledBullet( - "firestore", - `The following indexes are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}` - ); - } - - if (!shouldDeleteIndexes) { - shouldDeleteIndexes = await promptOnce({ - type: "confirm", - name: "confirm", - default: false, - message: - "Would you like to delete these indexes? Selecting no will continue the rest of the deployment.", - }); - } - } - - for (const index of indexesToDeploy) { - const exists = existingIndexes.some((x) => this.indexMatchesSpec(x, index)); - if (exists) { - logger.debug(`Skipping existing index: ${JSON.stringify(index)}`); - } else { - logger.debug(`Creating new index: ${JSON.stringify(index)}`); - await this.createIndex(options.project, index); - } - } - - if (shouldDeleteIndexes && indexesToDelete.length > 0) { - utils.logLabeledBullet("firestore", `Deleting ${indexesToDelete.length} indexes...`); - for (const index of indexesToDelete) { - await this.deleteIndex(index); - } - } - - let shouldDeleteFields = options.force; - if (fieldOverridesToDelete.length > 0) { - if (options.nonInteractive && !options.force) { - utils.logLabeledBullet( - "firestore", - `there are ${fieldOverridesToDelete.length} field overrides defined in your project that are not present in your ` + - "firestore indexes file. To delete them, run this command with the --force flag." - ); - } else if (!options.force) { - const indexesString = fieldOverridesToDelete - .map((x) => this.prettyFieldString(x)) - .join("\n\t"); - utils.logLabeledBullet( - "firestore", - `The following field overrides are defined in your project but are not present in your firestore indexes file:\n\t${indexesString}` - ); - } - - if (!shouldDeleteFields) { - shouldDeleteFields = await promptOnce({ - type: "confirm", - name: "confirm", - default: false, - message: - "Would you like to delete these field overrides? Selecting no will continue the rest of the deployment.", - }); - } - } - - for (const field of fieldOverridesToDeploy) { - const exists = existingFieldOverrides.some((x) => this.fieldMatchesSpec(x, field)); - if (exists) { - logger.debug(`Skipping existing field override: ${JSON.stringify(field)}`); - } else { - logger.debug(`Updating field override: ${JSON.stringify(field)}`); - await this.patchField(options.project, field); - } - } - - if (shouldDeleteFields && fieldOverridesToDelete.length > 0) { - utils.logLabeledBullet( - "firestore", - `Deleting ${fieldOverridesToDelete.length} field overrides...` - ); - for (const field of fieldOverridesToDelete) { - await this.deleteField(field); - } - } - } - - /** - * List all indexes that exist on a given project. - * @param project the Firebase project id. - */ - async listIndexes(project: string): Promise { - const url = `projects/${project}/databases/(default)/collectionGroups/-/indexes`; - - const res = await api.request("GET", `/v1/${url}`, { - auth: true, - origin: api.firestoreOrigin, - }); - - const indexes = res.body.indexes; - if (!indexes) { - return []; - } - - return indexes.map( - (index: any): API.Index => { - // Ignore any fields that point at the document ID, as those are implied - // in all indexes. - const fields = index.fields.filter((field: API.IndexField) => { - return field.fieldPath !== "__name__"; - }); - - return { - name: index.name, - state: index.state, - queryScope: index.queryScope, - fields, - }; - } - ); - } - - /** - * List all field configuration overrides defined on the given project. - * @param project the Firebase project. - */ - async listFieldOverrides(project: string): Promise { - const parent = `projects/${project}/databases/(default)/collectionGroups/-`; - const url = `${parent}/fields?filter=indexConfig.usesAncestorConfig=false`; - - const res = await api.request("GET", `/v1/${url}`, { - auth: true, - origin: api.firestoreOrigin, - }); - - const fields = res.body.fields as API.Field[]; - - // This should never be the case, since the API always returns the __default__ - // configuration, but this is a defensive check. - if (!fields) { - return []; - } - - // Ignore the default config, only list other fields. - return fields.filter((field) => { - return field.name.indexOf("__default__") < 0; - }); - } - - /** - * Turn an array of indexes and field overrides into a {@link Spec.IndexFile} suitable for use - * in an indexes.json file. - */ - makeIndexSpec(indexes: API.Index[], fields?: API.Field[]): Spec.IndexFile { - const indexesJson = indexes.map((index) => { - return { - collectionGroup: util.parseIndexName(index.name).collectionGroupId, - queryScope: index.queryScope, - fields: index.fields, - }; - }); - - if (!fields) { - logger.debug("No field overrides specified, using []."); - fields = []; - } - - const fieldsJson = fields.map((field) => { - const parsedName = util.parseFieldName(field.name); - const fieldIndexes = field.indexConfig.indexes || []; - return { - collectionGroup: parsedName.collectionGroupId, - fieldPath: parsedName.fieldPath, - - indexes: fieldIndexes.map((index) => { - const firstField = index.fields[0]; - return { - order: firstField.order, - arrayConfig: firstField.arrayConfig, - queryScope: index.queryScope, - }; - }), - }; - }); - - const sortedIndexes = indexesJson.sort(sort.compareSpecIndex); - const sortedFields = fieldsJson.sort(sort.compareFieldOverride); - return { - indexes: sortedIndexes, - fieldOverrides: sortedFields, - }; - } - - /** - * Print an array of indexes to the console. - * @param indexes the array of indexes. - */ - prettyPrintIndexes(indexes: API.Index[]): void { - if (indexes.length === 0) { - logger.info("None"); - return; - } - - const sortedIndexes = indexes.sort(sort.compareApiIndex); - sortedIndexes.forEach((index) => { - logger.info(this.prettyIndexString(index)); - }); - } - - /** - * Print an array of field overrides to the console. - * @param fields the array of field overrides. - */ - printFieldOverrides(fields: API.Field[]): void { - if (fields.length === 0) { - logger.info("None"); - return; - } - - const sortedFields = fields.sort(sort.compareApiField); - sortedFields.forEach((field) => { - logger.info(this.prettyFieldString(field)); - }); - } - - /** - * Validate that an object is a valid index specification. - * @param spec the object, normally parsed from JSON. - */ - validateSpec(spec: any): void { - validator.assertHas(spec, "indexes"); - - spec.indexes.forEach((index: any) => { - this.validateIndex(index); - }); - - if (spec.fieldOverrides) { - spec.fieldOverrides.forEach((field: any) => { - this.validateField(field); - }); - } - } - - /** - * Validate that an arbitrary object is safe to use as an {@link API.Field}. - */ - validateIndex(index: any): void { - validator.assertHas(index, "collectionGroup"); - validator.assertHas(index, "queryScope"); - validator.assertEnum(index, "queryScope", Object.keys(API.QueryScope)); - - validator.assertHas(index, "fields"); - - index.fields.forEach((field: any) => { - validator.assertHas(field, "fieldPath"); - validator.assertHasOneOf(field, ["order", "arrayConfig"]); - - if (field.order) { - validator.assertEnum(field, "order", Object.keys(API.Order)); - } - - if (field.arrayConfig) { - validator.assertEnum(field, "arrayConfig", Object.keys(API.ArrayConfig)); - } - }); - } - - /** - * Validate that an arbitrary object is safe to use as an {@link Spec.FieldOverride}. - * @param field - */ - validateField(field: any): void { - validator.assertHas(field, "collectionGroup"); - validator.assertHas(field, "fieldPath"); - validator.assertHas(field, "indexes"); - - field.indexes.forEach((index: any) => { - validator.assertHasOneOf(index, ["arrayConfig", "order"]); - - if (index.arrayConfig) { - validator.assertEnum(index, "arrayConfig", Object.keys(API.ArrayConfig)); - } - - if (index.order) { - validator.assertEnum(index, "order", Object.keys(API.Order)); - } - - if (index.queryScope) { - validator.assertEnum(index, "queryScope", Object.keys(API.QueryScope)); - } - }); - } - - /** - * Update the configuration of a field. Note that this kicks off a long-running - * operation for index creation/deletion so the update is complete when this - * method returns. - * @param project the Firebase project. - * @param spec the new field override specification. - */ - async patchField(project: string, spec: Spec.FieldOverride): Promise { - const url = `projects/${project}/databases/(default)/collectionGroups/${spec.collectionGroup}/fields/${spec.fieldPath}`; - - const indexes = spec.indexes.map((index) => { - return { - queryScope: index.queryScope, - fields: [ - { - fieldPath: spec.fieldPath, - arrayConfig: index.arrayConfig, - order: index.order, - }, - ], - }; - }); - - const data = { - indexConfig: { - indexes, - }, - }; - - await api.request("PATCH", `/v1/${url}`, { - auth: true, - origin: api.firestoreOrigin, - data, - }); - } - - /** - * Delete an existing index on the specified project. - */ - deleteField(field: API.Field): Promise { - const url = field.name; - const data = {}; - - return api.request("PATCH", "/v1/" + url + "?updateMask=indexConfig", { - auth: true, - origin: api.firestoreOrigin, - data, - }); - } - - /** - * Create a new index on the specified project. - */ - createIndex(project: string, index: Spec.Index): Promise { - const url = `projects/${project}/databases/(default)/collectionGroups/${index.collectionGroup}/indexes`; - return api.request("POST", "/v1/" + url, { - auth: true, - data: { - fields: index.fields, - queryScope: index.queryScope, - }, - origin: api.firestoreOrigin, - }); - } - - /** - * Delete an existing index on the specified project. - */ - deleteIndex(index: API.Index): Promise { - const url = index.name!; - return api.request("DELETE", "/v1/" + url, { - auth: true, - origin: api.firestoreOrigin, - }); - } - - /** - * Determine if an API Index and a Spec Index are functionally equivalent. - */ - indexMatchesSpec(index: API.Index, spec: Spec.Index): boolean { - const collection = util.parseIndexName(index.name).collectionGroupId; - if (collection !== spec.collectionGroup) { - return false; - } - - if (index.queryScope !== spec.queryScope) { - return false; - } - - if (index.fields.length !== spec.fields.length) { - return false; - } - - let i = 0; - while (i < index.fields.length) { - const iField = index.fields[i]; - const sField = spec.fields[i]; - - if (iField.fieldPath !== sField.fieldPath) { - return false; - } - - if (iField.order !== sField.order) { - return false; - } - - if (iField.arrayConfig !== sField.arrayConfig) { - return false; - } - - i++; - } - - return true; - } - - /** - * Determine if an API Field and a Spec Field are functionally equivalent. - */ - fieldMatchesSpec(field: API.Field, spec: Spec.FieldOverride): boolean { - const parsedName = util.parseFieldName(field.name); - - if (parsedName.collectionGroupId !== spec.collectionGroup) { - return false; - } - - if (parsedName.fieldPath !== spec.fieldPath) { - return false; - } - - const fieldIndexes = field.indexConfig.indexes || []; - if (fieldIndexes.length !== spec.indexes.length) { - return false; - } - - const fieldModes = fieldIndexes.map((index) => { - const firstField = index.fields[0]; - return firstField.order || firstField.arrayConfig; - }); - - const specModes = spec.indexes.map((index) => { - return index.order || index.arrayConfig; - }); - - // Confirms that the two objects have the same set of enabled indexes without - // caring about specification order. - for (const mode of fieldModes) { - if (specModes.indexOf(mode) < 0) { - return false; - } - } - - return true; - } - - /** - * Take a object that may represent an old v1beta1 indexes spec - * and convert it to the new v1/v1 spec format. - * - * This function is meant to be run **before** validation and - * works on a purely best-effort basis. - */ - upgradeOldSpec(spec: any): any { - const result = { - indexes: [], - fieldOverrides: spec.fieldOverrides || [], - }; - - if (!(spec.indexes && spec.indexes.length > 0)) { - return result; - } - - // Try to detect use of the old API, warn the users. - if (spec.indexes[0].collectionId) { - utils.logBullet( - clc.bold.cyan("firestore:") + - " your indexes indexes are specified in the v1beta1 API format. " + - "Please upgrade to the new index API format by running " + - clc.bold("firebase firestore:indexes") + - " again and saving the result." - ); - } - - result.indexes = spec.indexes.map((index: any) => { - const i = { - collectionGroup: index.collectionGroup || index.collectionId, - queryScope: index.queryScope || API.QueryScope.COLLECTION, - fields: [], - }; - - if (index.fields) { - i.fields = index.fields.map((field: any) => { - const f: any = { - fieldPath: field.fieldPath, - }; - - if (field.order) { - f.order = field.order; - } else if (field.arrayConfig) { - f.arrayConfig = field.arrayConfig; - } else if (field.mode === API.Mode.ARRAY_CONTAINS) { - f.arrayConfig = API.ArrayConfig.CONTAINS; - } else { - f.order = field.mode; - } - - return f; - }); - } - - return i; - }); - - return result; - } - - /** - * Get a colored, pretty-printed representation of an index. - */ - private prettyIndexString(index: API.Index, includeState: boolean = true): string { - let result = ""; - - if (index.state && includeState) { - const stateMsg = `[${index.state}] `; - - if (index.state === API.State.READY) { - result += clc.green(stateMsg); - } else if (index.state === API.State.CREATING) { - result += clc.yellow(stateMsg); - } else { - result += clc.red(stateMsg); - } - } - - const nameInfo = util.parseIndexName(index.name); - - result += clc.cyan(`(${nameInfo.collectionGroupId})`); - result += " -- "; - - index.fields.forEach((field) => { - if (field.fieldPath === "__name__") { - return; - } - - // Normal field indexes have an "order" while array indexes have an "arrayConfig", - // we want to display whichever one is present. - const orderOrArrayConfig = field.order ? field.order : field.arrayConfig; - result += `(${field.fieldPath},${orderOrArrayConfig}) `; - }); - - return result; - } - - /** - * Get a colored, pretty-printed representation of a field - */ - private prettyFieldString(field: API.Field): string { - let result = ""; - - const parsedName = util.parseFieldName(field.name); - - result += - "[" + - clc.cyan(parsedName.collectionGroupId) + - "." + - clc.yellow(parsedName.fieldPath) + - "] --"; - - const fieldIndexes = field.indexConfig.indexes || []; - if (fieldIndexes.length > 0) { - fieldIndexes.forEach((index) => { - const firstField = index.fields[0]; - const mode = firstField.order || firstField.arrayConfig; - result += ` (${mode})`; - }); - } else { - result += " (no indexes)"; - } - - return result; - } -} diff --git a/src/firestore/options.ts b/src/firestore/options.ts new file mode 100644 index 00000000000..6e227bae866 --- /dev/null +++ b/src/firestore/options.ts @@ -0,0 +1,30 @@ +import { Options } from "../options"; +import { DayOfWeek } from "../gcp/firestore"; +import * as types from "../firestore/api-types"; + +/** + * The set of fields that the Firestore commands need from Options. + * It is preferable that all codebases use this technique so that they keep + * strong typing in their codebase but limit the codebase to have less to mock. + */ +export interface FirestoreOptions extends Options { + project: string; + database?: string; + nonInteractive: boolean; + allCollections?: boolean; + shallow?: boolean; + recursive?: boolean; + location?: string; + type?: types.DatabaseType; + deleteProtection?: types.DatabaseDeleteProtectionStateOption; + pointInTimeRecoveryEnablement?: types.PointInTimeRecoveryEnablementOption; + + // backup schedules + backupSchedule?: string; + retention?: `${number}${"h" | "d" | "m" | "w"}`; + recurrence?: types.RecurrenceType; + dayOfWeek?: DayOfWeek; + + // backups + backup?: string; +} diff --git a/src/firestore/pretty-print.test.ts b/src/firestore/pretty-print.test.ts new file mode 100644 index 00000000000..8382f840c20 --- /dev/null +++ b/src/firestore/pretty-print.test.ts @@ -0,0 +1,82 @@ +import { expect } from "chai"; +import * as API from "./api-types"; +import { PrettyPrint } from "./pretty-print"; + +const printer = new PrettyPrint(); + +describe("prettyIndexString", () => { + it("should correctly print an order type Index", () => { + expect( + printer.prettyIndexString( + { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/a", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", order: API.Order.DESCENDING }, + ], + }, + false, + ), + ).to.contain("(foo,ASCENDING) (bar,DESCENDING) "); + }); + + it("should correctly print a contains type Index", () => { + expect( + printer.prettyIndexString( + { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/a", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "baz", arrayConfig: API.ArrayConfig.CONTAINS }, + ], + }, + false, + ), + ).to.contain("(foo,ASCENDING) (baz,CONTAINS) "); + }); + + it("should correctly print a vector type Index", () => { + expect( + printer.prettyIndexString( + { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/a", + queryScope: API.QueryScope.COLLECTION, + fields: [{ fieldPath: "foo", vectorConfig: { dimension: 100, flat: {} } }], + }, + false, + ), + ).to.contain("(foo,VECTOR<100>) "); + }); + + it("should correctly print a vector type Index with other fields", () => { + expect( + printer.prettyIndexString( + { + name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/a", + queryScope: API.QueryScope.COLLECTION, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", vectorConfig: { dimension: 200, flat: {} } }, + ], + }, + false, + ), + ).to.contain("(foo,ASCENDING) (bar,VECTOR<200>) "); + }); +}); + +describe("firebaseConsoleDatabaseUrl", () => { + it("should provide a console link", () => { + expect(printer.firebaseConsoleDatabaseUrl("example-project", "example-db")).to.equal( + "https://console.firebase.google.com/project/example-project/firestore/databases/example-db/data", + ); + }); + + it("should convert (default) to -default-", () => { + expect(printer.firebaseConsoleDatabaseUrl("example-project", "(default)")).to.equal( + "https://console.firebase.google.com/project/example-project/firestore/databases/-default-/data", + ); + }); +}); diff --git a/src/firestore/pretty-print.ts b/src/firestore/pretty-print.ts new file mode 100644 index 00000000000..e88fef5175a --- /dev/null +++ b/src/firestore/pretty-print.ts @@ -0,0 +1,318 @@ +import * as clc from "colorette"; +const Table = require("cli-table"); + +import * as sort from "./api-sort"; +import * as types from "./api-types"; +import { logger } from "../logger"; +import * as util from "./util"; +import { consoleUrl } from "../utils"; +import { Backup, BackupSchedule } from "../gcp/firestore"; + +export class PrettyPrint { + /** + * Print an array of indexes to the console. + * @param indexes the array of indexes. + */ + prettyPrintIndexes(indexes: types.Index[]): void { + if (indexes.length === 0) { + logger.info("None"); + return; + } + + const sortedIndexes = indexes.sort(sort.compareApiIndex); + sortedIndexes.forEach((index) => { + logger.info(this.prettyIndexString(index)); + }); + } + + /** + * Print an array of databases to the console as an ASCII table. + * @param databases the array of Firestore databases. + */ + prettyPrintDatabases(databases: types.DatabaseResp[]): void { + if (databases.length === 0) { + logger.info("No databases found."); + return; + } + const sortedDatabases: types.DatabaseResp[] = databases.sort(sort.compareApiDatabase); + const table = new Table({ + head: ["Database Name"], + colWidths: [Math.max(...sortedDatabases.map((database) => database.name.length + 5), 20)], + }); + + table.push(...sortedDatabases.map((database) => [this.prettyDatabaseString(database)])); + logger.info(table.toString()); + } + + /** + * Print important fields of a database to the console as an ASCII table. + * @param database the Firestore database. + */ + prettyPrintDatabase(database: types.DatabaseResp): void { + const table = new Table({ + head: ["Field", "Value"], + colWidths: [25, Math.max(50, 5 + database.name.length)], + }); + + table.push( + ["Name", clc.yellow(database.name)], + ["Create Time", clc.yellow(database.createTime)], + ["Last Update Time", clc.yellow(database.updateTime)], + ["Type", clc.yellow(database.type)], + ["Location", clc.yellow(database.locationId)], + ["Delete Protection State", clc.yellow(database.deleteProtectionState)], + ["Point In Time Recovery", clc.yellow(database.pointInTimeRecoveryEnablement)], + ["Earliest Version Time", clc.yellow(database.earliestVersionTime)], + ["Version Retention Period", clc.yellow(database.versionRetentionPeriod)], + ); + logger.info(table.toString()); + } + + /** + * Print an array of backups to the console as an ASCII table. + * @param backups the array of Firestore backups. + */ + prettyPrintBackups(backups: Backup[]): void { + if (backups.length === 0) { + logger.info("No backups found."); + return; + } + const sortedBackups: Backup[] = backups.sort(sort.compareApiBackup); + const table = new Table({ + head: ["Backup Name", "Database Name", "Snapshot Time", "State"], + colWidths: [ + Math.max(...sortedBackups.map((backup) => backup.name!.length + 5), 20), + Math.max(...sortedBackups.map((backup) => backup.database!.length + 5), 20), + 30, + 10, + ], + }); + + table.push( + ...sortedBackups.map((backup) => [ + this.prettyBackupString(backup), + this.prettyDatabaseString(backup.database || ""), + backup.snapshotTime, + backup.state, + ]), + ); + logger.info(table.toString()); + } + + /** + * Print an array of backup schedules to the console as an ASCII table. + * @param backupSchedules the array of Firestore backup schedules. + * @param databaseId the database these schedules are associated with. + */ + prettyPrintBackupSchedules(backupSchedules: BackupSchedule[], databaseId: string): void { + if (backupSchedules.length === 0) { + logger.info(`No backup schedules for database ${databaseId} found.`); + return; + } + const sortedBackupSchedules: BackupSchedule[] = backupSchedules.sort( + sort.compareApiBackupSchedule, + ); + sortedBackupSchedules.forEach((schedule) => this.prettyPrintBackupSchedule(schedule)); + } + + /** + * Print important fields of a backup schedule to the console as an ASCII table. + * @param backupSchedule the Firestore backup schedule. + */ + prettyPrintBackupSchedule(backupSchedule: BackupSchedule): void { + const table = new Table({ + head: ["Field", "Value"], + colWidths: [25, Math.max(50, 5 + backupSchedule.name!.length)], + }); + + table.push( + ["Name", clc.yellow(backupSchedule.name!)], + ["Create Time", clc.yellow(backupSchedule.createTime!)], + ["Last Update Time", clc.yellow(backupSchedule.updateTime!)], + ["Retention", clc.yellow(backupSchedule.retention)], + ["Recurrence", this.prettyRecurrenceString(backupSchedule)], + ); + logger.info(table.toString()); + } + + /** + * Returns a pretty representation of the Recurrence of the given backup schedule. + * @param {BackupSchedule} backupSchedule the backup schedule. + */ + prettyRecurrenceString(backupSchedule: BackupSchedule): string { + if (backupSchedule.dailyRecurrence) { + return clc.yellow("DAILY"); + } else if (backupSchedule.weeklyRecurrence) { + return clc.yellow(`WEEKLY (${backupSchedule.weeklyRecurrence.day})`); + } + return ""; + } + + /** + * Print important fields of a backup to the console as an ASCII table. + * @param backup the Firestore backup. + */ + prettyPrintBackup(backup: Backup) { + const table = new Table({ + head: ["Field", "Value"], + colWidths: [25, Math.max(50, 5 + backup.name!.length, 5 + backup.database!.length)], + }); + + table.push( + ["Name", clc.yellow(backup.name!)], + ["Database", clc.yellow(backup.database!)], + ["Database UID", clc.yellow(backup.databaseUid!)], + ["State", clc.yellow(backup.state!)], + ["Snapshot Time", clc.yellow(backup.snapshotTime!)], + ["Expire Time", clc.yellow(backup.expireTime!)], + ["Stats", clc.yellow(backup.stats!)], + ); + logger.info(table.toString()); + } + + /** + * Print an array of locations to the console in an ASCII table. Group multi regions together + * Example: United States: nam5 + * @param locations the array of locations. + */ + prettyPrintLocations(locations: types.Location[]): void { + if (locations.length === 0) { + logger.info("No Locations Available"); + return; + } + const table = new Table({ + head: ["Display Name", "LocationId"], + colWidths: [20, 30], + }); + + table.push( + ...locations + .sort(sort.compareLocation) + .map((location) => [location.displayName, location.locationId]), + ); + logger.info(table.toString()); + } + + /** + * Print an array of field overrides to the console. + * @param fields the array of field overrides. + */ + printFieldOverrides(fields: types.Field[]): void { + if (fields.length === 0) { + logger.info("None"); + return; + } + + const sortedFields = fields.sort(sort.compareApiField); + sortedFields.forEach((field) => { + logger.info(this.prettyFieldString(field)); + }); + } + + /** + * Get a colored, pretty-printed representation of an index. + */ + prettyIndexString(index: types.Index, includeState = true): string { + let result = ""; + + if (index.state && includeState) { + const stateMsg = `[${index.state}] `; + + if (index.state === types.State.READY) { + result += clc.green(stateMsg); + } else if (index.state === types.State.CREATING) { + result += clc.yellow(stateMsg); + } else { + result += clc.red(stateMsg); + } + } + + const nameInfo = util.parseIndexName(index.name); + + result += clc.cyan(`(${nameInfo.collectionGroupId})`); + result += " -- "; + + index.fields.forEach((field) => { + if (field.fieldPath === "__name__") { + return; + } + + // Normal field indexes have an "order", array indexes have an + // "arrayConfig", and vector indexes have a "vectorConfig" we want to + // display whichever one is present. + let configString; + if (field.order) { + configString = field.order; + } else if (field.arrayConfig) { + configString = field.arrayConfig; + } else if (field.vectorConfig) { + configString = `VECTOR<${field.vectorConfig.dimension}>`; + } + result += `(${field.fieldPath},${configString}) `; + }); + + return result; + } + + /** + * Get a colored, pretty-printed representation of a backup + */ + prettyBackupString(backup: Backup): string { + return clc.yellow(backup.name || ""); + } + + /** + * Get a colored, pretty-printed representation of a backup schedule + */ + prettyBackupScheduleString(backupSchedule: BackupSchedule): string { + return clc.yellow(backupSchedule.name || ""); + } + + /** + * Get a colored, pretty-printed representation of a database + */ + prettyDatabaseString(database: string | types.DatabaseResp): string { + return clc.yellow(typeof database === "string" ? database : database.name); + } + + /** + * Get a URL to view a given Firestore database in the Firebase console + */ + firebaseConsoleDatabaseUrl(project: string, databaseId: string): string { + const urlFriendlyDatabaseId = databaseId === "(default)" ? "-default-" : databaseId; + return consoleUrl(project, `/firestore/databases/${urlFriendlyDatabaseId}/data`); + } + + /** + * Get a colored, pretty-printed representation of a field + */ + prettyFieldString(field: types.Field): string { + let result = ""; + + const parsedName = util.parseFieldName(field.name); + + result += + "[" + + clc.cyan(parsedName.collectionGroupId) + + "." + + clc.yellow(parsedName.fieldPath) + + "] --"; + + const fieldIndexes = field.indexConfig.indexes || []; + if (fieldIndexes.length > 0) { + fieldIndexes.forEach((index) => { + const firstField = index.fields[0]; + const mode = firstField.order || firstField.arrayConfig; + result += ` (${mode})`; + }); + } else { + result += " (no indexes)"; + } + const fieldTtl = field.ttlConfig; + if (fieldTtl) { + result += ` TTL(${fieldTtl.state})`; + } + + return result; + } +} diff --git a/src/firestore/util.test.ts b/src/firestore/util.test.ts new file mode 100644 index 00000000000..a12fbd8e4a1 --- /dev/null +++ b/src/firestore/util.test.ts @@ -0,0 +1,49 @@ +import { expect } from "chai"; + +import * as util from "./util"; + +describe("IndexNameParsing", () => { + it("should parse an index name correctly", () => { + const name = + "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123/"; + expect(util.parseIndexName(name)).to.eql({ + projectId: "myproject", + databaseId: "(default)", + collectionGroupId: "collection", + indexId: "abc123", + }); + }); + + it("should parse a field name correctly", () => { + const name = + "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123/"; + expect(util.parseFieldName(name)).to.eql({ + projectId: "myproject", + databaseId: "(default)", + collectionGroupId: "collection", + fieldPath: "abc123", + }); + }); + + it("should parse an index name from a named database correctly", () => { + const name = + "/projects/myproject/databases/named-db/collectionGroups/collection/indexes/abc123/"; + expect(util.parseIndexName(name)).to.eql({ + projectId: "myproject", + databaseId: "named-db", + collectionGroupId: "collection", + indexId: "abc123", + }); + }); + + it("should parse a field name from a named database correctly", () => { + const name = + "/projects/myproject/databases/named-db/collectionGroups/collection/fields/abc123/"; + expect(util.parseFieldName(name)).to.eql({ + projectId: "myproject", + databaseId: "named-db", + collectionGroupId: "collection", + fieldPath: "abc123", + }); + }); +}); diff --git a/src/firestore/util.ts b/src/firestore/util.ts index 8368ed37e97..96f342e2edf 100644 --- a/src/firestore/util.ts +++ b/src/firestore/util.ts @@ -2,21 +2,25 @@ import { FirebaseError } from "../error"; interface IndexName { projectId: string; + databaseId: string; collectionGroupId: string; indexId: string; } interface FieldName { projectId: string; + databaseId: string; collectionGroupId: string; fieldPath: string; } -// projects/$PROJECT_ID/databases/(default)/collectionGroups/$COLLECTION_GROUP_ID/indexes/$INDEX_ID -const INDEX_NAME_REGEX = /projects\/([^\/]+?)\/databases\/\(default\)\/collectionGroups\/([^\/]+?)\/indexes\/([^\/]*)/; +// projects/$PROJECT_ID/databases/$DATABASE_ID/collectionGroups/$COLLECTION_GROUP_ID/indexes/$INDEX_ID +const INDEX_NAME_REGEX = + /projects\/([^\/]+?)\/databases\/([^\/]+?)\/collectionGroups\/([^\/]+?)\/indexes\/([^\/]*)/; -// projects/$PROJECT_ID/databases/(default)/collectionGroups/$COLLECTION_GROUP_ID/fields/$FIELD_ID -const FIELD_NAME_REGEX = /projects\/([^\/]+?)\/databases\/\(default\)\/collectionGroups\/([^\/]+?)\/fields\/([^\/]*)/; +// projects/$PROJECT_ID/databases/$DATABASE_ID/collectionGroups/$COLLECTION_GROUP_ID/fields/$FIELD_ID +const FIELD_NAME_REGEX = + /projects\/([^\/]+?)\/databases\/([^\/]+?)\/collectionGroups\/([^\/]+?)\/fields\/([^\/]*)/; /** * Parse an Index name into useful pieces. @@ -27,14 +31,15 @@ export function parseIndexName(name?: string): IndexName { } const m = name.match(INDEX_NAME_REGEX); - if (!m || m.length < 4) { + if (!m || m.length < 5) { throw new FirebaseError(`Error parsing index name: ${name}`); } return { projectId: m[1], - collectionGroupId: m[2], - indexId: m[3], + databaseId: m[2], + collectionGroupId: m[3], + indexId: m[4], }; } @@ -49,7 +54,15 @@ export function parseFieldName(name: string): FieldName { return { projectId: m[1], - collectionGroupId: m[2], - fieldPath: m[3], + databaseId: m[2], + collectionGroupId: m[3], + fieldPath: m[4], }; } + +/** + * Performs XOR operator between two boolean values + */ +export function booleanXOR(a: boolean, b: boolean): boolean { + return !!(Number(a) - Number(b)); +} diff --git a/src/firestore/validator.ts b/src/firestore/validator.ts index 42dfc41425b..87a3169c141 100644 --- a/src/firestore/validator.ts +++ b/src/firestore/validator.ts @@ -1,4 +1,4 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { FirebaseError } from "../error"; /** @@ -39,3 +39,13 @@ export function assertEnum(obj: any, prop: string, valid: any[]): void { throw new FirebaseError(`Field "${prop}" must be one of ${valid.join(", ")}: ${objString}`); } } + +/** + * Throw an error if the value of the property 'prop' differs against type + * guard. + */ +export function assertType(prop: string, propValue: any, type: string): void { + if (typeof propValue !== type) { + throw new FirebaseError(`Property "${prop}" must be of type ${type}`); + } +} diff --git a/src/frameworks/angular/index.spec.ts b/src/frameworks/angular/index.spec.ts new file mode 100644 index 00000000000..f3de64863fe --- /dev/null +++ b/src/frameworks/angular/index.spec.ts @@ -0,0 +1,28 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as fsExtra from "fs-extra"; + +import { discover } from "."; + +describe("Angular", () => { + describe("discovery", () => { + const cwd = Math.random().toString(36).split(".")[1]; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should find an Angular app", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(true); + expect(await discover(cwd)).to.deep.equal({ + mayWantBackend: true, + version: undefined, + }); + }); + }); +}); diff --git a/src/frameworks/angular/index.ts b/src/frameworks/angular/index.ts new file mode 100644 index 00000000000..772e090b391 --- /dev/null +++ b/src/frameworks/angular/index.ts @@ -0,0 +1,257 @@ +import { join, posix } from "path"; +import { execSync } from "child_process"; +import { spawn, sync as spawnSync } from "cross-spawn"; +import { copy, pathExists } from "fs-extra"; +import { mkdir } from "fs/promises"; + +import { + BuildResult, + Discovery, + FrameworkType, + SupportLevel, + BUILD_TARGET_PURPOSE, +} from "../interfaces"; +import { + simpleProxy, + relativeRequire, + getNodeModuleBin, + warnIfCustomBuildScript, + findDependency, +} from "../utils"; +import { + getAllTargets, + getAngularVersion, + getBrowserConfig, + getBuildConfig, + getContext, + getServerConfig, +} from "./utils"; +import { I18N_ROOT, SHARP_VERSION } from "../constants"; +import { FirebaseError } from "../../error"; + +export const name = "Angular"; +export const support = SupportLevel.Preview; +export const type = FrameworkType.Framework; +export const docsUrl = "https://firebase.google.com/docs/hosting/frameworks/angular"; + +const DEFAULT_BUILD_SCRIPT = ["ng build"]; + +export const supportedRange = "14 - 17"; + +export async function discover(dir: string): Promise { + if (!(await pathExists(join(dir, "package.json")))) return; + if (!(await pathExists(join(dir, "angular.json")))) return; + const version = getAngularVersion(dir); + return { mayWantBackend: true, version }; +} + +export function init(setup: any, config: any) { + execSync( + `npx --yes -p @angular/cli@"${supportedRange}" ng new ${setup.projectId} --directory ${setup.hosting.source} --skip-git`, + { + stdio: "inherit", + cwd: config.projectDir, + }, + ); + return Promise.resolve(); +} + +export async function build(dir: string, configuration: string): Promise { + const { + targets, + serveOptimizedImages, + locales, + baseHref: baseUrl, + ssr, + } = await getBuildConfig(dir, configuration); + await warnIfCustomBuildScript(dir, name, DEFAULT_BUILD_SCRIPT); + for (const target of targets) { + // TODO there is a bug here. Spawn for now. + // await scheduleTarget(prerenderTarget); + const cli = getNodeModuleBin("ng", dir); + const result = spawnSync(cli, ["run", target], { + cwd: dir, + stdio: "inherit", + }); + if (result.status !== 0) throw new FirebaseError(`Unable to build ${target}`); + } + + const wantsBackend = ssr || serveOptimizedImages; + const rewrites = ssr + ? [] + : [ + { + source: posix.join(baseUrl, "**"), + destination: posix.join(baseUrl, "index.html"), + }, + ]; + const i18n = !!locales; + return { wantsBackend, i18n, rewrites, baseUrl }; +} + +export async function getDevModeHandle(dir: string, configuration: string) { + const { targetStringFromTarget } = await relativeRequire(dir, "@angular-devkit/architect"); + const { serveTarget } = await getContext(dir, configuration); + if (!serveTarget) throw new Error("Could not find the serveTarget"); + const host = new Promise((resolve, reject) => { + // Can't use scheduleTarget since that—like prerender—is failing on an ESM bug + // will just grep for the hostname + const cli = getNodeModuleBin("ng", dir); + const serve = spawn(cli, ["run", targetStringFromTarget(serveTarget), "--host", "localhost"], { + cwd: dir, + }); + serve.stdout.on("data", (data: any) => { + process.stdout.write(data); + const match = data.toString().match(/(http:\/\/localhost:\d+)/); + if (match) resolve(match[1]); + }); + serve.stderr.on("data", (data: any) => { + process.stderr.write(data); + }); + serve.on("exit", reject); + }); + return simpleProxy(await host); +} + +export async function ɵcodegenPublicDirectory( + sourceDir: string, + destDir: string, + configuration: string, +) { + const { outputPath, baseHref, defaultLocale, locales } = await getBrowserConfig( + sourceDir, + configuration, + ); + await mkdir(join(destDir, baseHref), { recursive: true }); + if (locales) { + await Promise.all([ + defaultLocale + ? await copy(join(sourceDir, outputPath, defaultLocale), join(destDir, baseHref)) + : Promise.resolve(), + ...locales.map(async (locale) => { + await mkdir(join(destDir, I18N_ROOT, locale, baseHref), { recursive: true }); + await copy(join(sourceDir, outputPath, locale), join(destDir, I18N_ROOT, locale, baseHref)); + }), + ]); + } else { + await copy(join(sourceDir, outputPath), join(destDir, baseHref)); + } +} + +export async function getValidBuildTargets(purpose: BUILD_TARGET_PURPOSE, dir: string) { + const validTargetNames = new Set(["development", "production"]); + try { + const { workspaceProject, buildTarget, browserTarget, prerenderTarget, serveTarget } = + await getContext(dir); + const { target } = ((purpose === "emulate" && serveTarget) || + buildTarget || + prerenderTarget || + browserTarget)!; + const workspaceTarget = workspaceProject.targets.get(target)!; + Object.keys(workspaceTarget.configurations || {}).forEach((it) => validTargetNames.add(it)); + } catch (e) { + // continue + } + const allTargets = await getAllTargets(purpose, dir); + return [...validTargetNames, ...allTargets]; +} + +export async function shouldUseDevModeHandle(targetOrConfiguration: string, dir: string) { + const { serveTarget } = await getContext(dir, targetOrConfiguration); + if (!serveTarget) return false; + return serveTarget.configuration !== "production"; +} + +export async function ɵcodegenFunctionsDirectory( + sourceDir: string, + destDir: string, + configuration: string, +) { + const { + packageJson, + serverOutputPath, + browserOutputPath, + defaultLocale, + serverLocales, + browserLocales, + bundleDependencies, + externalDependencies, + baseHref, + serveOptimizedImages, + serverEntry, + } = await getServerConfig(sourceDir, configuration); + + const dotEnv = { __NG_BROWSER_OUTPUT_PATH__: browserOutputPath }; + let rewriteSource: string | undefined = undefined; + + await Promise.all([ + serverOutputPath + ? mkdir(join(destDir, serverOutputPath), { recursive: true }).then(() => + copy(join(sourceDir, serverOutputPath), join(destDir, serverOutputPath)), + ) + : Promise.resolve(), + mkdir(join(destDir, browserOutputPath), { recursive: true }).then(() => + copy(join(sourceDir, browserOutputPath), join(destDir, browserOutputPath)), + ), + ]); + + if (bundleDependencies) { + const dependencies: Record = {}; + for (const externalDependency of externalDependencies) { + const packageVersion = findDependency(externalDependency)?.version; + if (packageVersion) { + dependencies[externalDependency] = packageVersion; + } + } + packageJson.dependencies = dependencies; + } else if (serverOutputPath) { + packageJson.dependencies ||= {}; + } else { + packageJson.dependencies = {}; + } + + if (serveOptimizedImages) { + packageJson.dependencies["sharp"] ||= SHARP_VERSION; + } + + let bootstrapScript: string; + if (browserLocales) { + const locales = serverLocales?.filter((it) => browserLocales.includes(it)); + bootstrapScript = `const localizedApps = new Map(); +const ffi18n = import("firebase-frameworks/i18n"); +exports.handle = function(req,res) { + ffi18n.then(({ getPreferredLocale }) => { + const locale = ${ + locales + ? `getPreferredLocale(req, ${JSON.stringify(locales)}, ${JSON.stringify(defaultLocale)})` + : `""` + }; + if (localizedApps.has(locale)) { + localizedApps.get(locale)(req,res); + } else { + ${ + serverEntry?.endsWith(".mjs") + ? `import(\`./${serverOutputPath}/\${locale}/${serverEntry}\`)` + : `Promise.resolve(require(\`./${serverOutputPath}/\${locale}/${serverEntry}\`))` + }.then(server => { + const app = server.app(locale); + localizedApps.set(locale, app); + app(req,res); + }); + } + }); +};\n`; + } else if (serverOutputPath) { + bootstrapScript = `const app = ${ + serverEntry?.endsWith(".mjs") + ? `import(\`./${serverOutputPath}/${serverEntry}\`)` + : `Promise.resolve(require('./${serverOutputPath}/${serverEntry}'))` + }.then(server => server.app()); +exports.handle = (req,res) => app.then(it => it(req,res));\n`; + } else { + bootstrapScript = `exports.handle = (res, req) => req.sendStatus(404);\n`; + rewriteSource = posix.join(baseHref, "__image__"); + } + + return { bootstrapScript, packageJson, dotEnv, rewriteSource }; +} diff --git a/src/frameworks/angular/interfaces.ts b/src/frameworks/angular/interfaces.ts new file mode 100644 index 00000000000..fe90421eb89 --- /dev/null +++ b/src/frameworks/angular/interfaces.ts @@ -0,0 +1,14 @@ +interface AngularLocale { + translation?: string; + baseHref?: string; +} + +export interface AngularI18nConfig { + sourceLocale: + | string + | { + code: string; + baseHref?: string; + }; + locales: Record; +} diff --git a/src/frameworks/angular/utils.ts b/src/frameworks/angular/utils.ts new file mode 100644 index 00000000000..74f06ee9378 --- /dev/null +++ b/src/frameworks/angular/utils.ts @@ -0,0 +1,563 @@ +import type { Target } from "@angular-devkit/architect"; +import type { ProjectDefinition } from "@angular-devkit/core/src/workspace"; +import type { WorkspaceNodeModulesArchitectHost } from "@angular-devkit/architect/node"; + +import { AngularI18nConfig } from "./interfaces"; +import { findDependency, relativeRequire, validateLocales } from "../utils"; +import { FirebaseError } from "../../error"; +import { join, posix, sep } from "path"; +import { BUILD_TARGET_PURPOSE } from "../interfaces"; +import { AssertionError } from "assert"; +import { assertIsString } from "../../utils"; +import { coerce } from "semver"; + +async function localesForTarget( + dir: string, + architectHost: WorkspaceNodeModulesArchitectHost, + target: Target, + workspaceProject: ProjectDefinition, +) { + const { targetStringFromTarget } = await relativeRequire(dir, "@angular-devkit/architect"); + const targetOptions = await architectHost.getOptionsForTarget(target); + if (!targetOptions) { + const targetString = targetStringFromTarget(target); + throw new FirebaseError(`Couldn't find options for ${targetString}.`); + } + + let locales: string[] | undefined = undefined; + let defaultLocale: string | undefined = undefined; + if (targetOptions.localize) { + const i18n: AngularI18nConfig | undefined = workspaceProject.extensions?.i18n as any; + if (!i18n) throw new FirebaseError(`No i18n config on project.`); + if (typeof i18n.sourceLocale === "string") { + throw new FirebaseError(`All your i18n locales must have a baseHref of "" on Firebase, use an object for sourceLocale in your angular.json: + "i18n": { + "sourceLocale": { + "code": "${i18n.sourceLocale}", + "baseHref": "" + }, + ... + }`); + } + if (i18n.sourceLocale.baseHref !== "") + throw new FirebaseError( + 'All your i18n locales must have a baseHref of "" on Firebase, errored on sourceLocale.', + ); + defaultLocale = i18n.sourceLocale.code; + if (targetOptions.localize === true) { + locales = [defaultLocale]; + for (const [locale, { baseHref }] of Object.entries(i18n.locales)) { + if (baseHref !== "") + throw new FirebaseError( + `All your i18n locales must have a baseHref of \"\" on Firebase, errored on ${locale}.`, + ); + locales.push(locale); + } + } else if (Array.isArray(targetOptions.localize)) { + locales = [defaultLocale]; + for (const locale of targetOptions.localize) { + if (typeof locale !== "string") continue; + locales.push(locale); + } + } + } + validateLocales(locales); + return { locales, defaultLocale }; +} + +const enum ExpectedBuilder { + APPLICATION = "@angular-devkit/build-angular:application", + BROWSER_ESBUILD = "@angular-devkit/build-angular:browser-esbuild", + DEPLOY = "@angular/fire:deploy", + DEV_SERVER = "@angular-devkit/build-angular:dev-server", + LEGACY_BROWSER = "@angular-devkit/build-angular:browser", + LEGACY_NGUNIVERSAL_PRERENDER = "@nguniversal/builders:prerender", + LEGACY_DEVKIT_PRERENDER = "@angular-devkit/build-angular:prerender", + LEGACY_SERVER = "@angular-devkit/build-angular:server", + LEGACY_NGUNIVERSAL_SSR_DEV_SERVER = "@nguniversal/builders:ssr-dev-server", + LEGACY_DEVKIT_SSR_DEV_SERVER = "@angular-devkit/build-angular:ssr-dev-server", +} + +const DEV_SERVER_TARGETS: string[] = [ + ExpectedBuilder.DEV_SERVER, + ExpectedBuilder.LEGACY_NGUNIVERSAL_SSR_DEV_SERVER, + ExpectedBuilder.LEGACY_DEVKIT_SSR_DEV_SERVER, +]; + +function getValidBuilders(purpose: BUILD_TARGET_PURPOSE): string[] { + return [ + ExpectedBuilder.APPLICATION, + ExpectedBuilder.BROWSER_ESBUILD, + ExpectedBuilder.DEPLOY, + ExpectedBuilder.LEGACY_BROWSER, + ExpectedBuilder.LEGACY_DEVKIT_PRERENDER, + ExpectedBuilder.LEGACY_NGUNIVERSAL_PRERENDER, + ...(purpose === "deploy" ? [] : DEV_SERVER_TARGETS), + ]; +} + +export async function getAllTargets(purpose: BUILD_TARGET_PURPOSE, dir: string) { + const validBuilders = getValidBuilders(purpose); + const [{ NodeJsAsyncHost }, { workspaces }, { targetStringFromTarget }] = await Promise.all([ + relativeRequire(dir, "@angular-devkit/core/node"), + relativeRequire(dir, "@angular-devkit/core"), + relativeRequire(dir, "@angular-devkit/architect"), + ]); + const host = workspaces.createWorkspaceHost(new NodeJsAsyncHost()); + const { workspace } = await workspaces.readWorkspace(dir, host); + + const targets: string[] = []; + workspace.projects.forEach((projectDefinition, project) => { + if (projectDefinition.extensions.projectType !== "application") return; + projectDefinition.targets.forEach((targetDefinition, target) => { + if (!validBuilders.includes(targetDefinition.builder)) return; + const configurations = Object.keys(targetDefinition.configurations || {}); + if (!configurations.includes("production")) configurations.push("production"); + if (!configurations.includes("development")) configurations.push("development"); + configurations.forEach((configuration) => { + targets.push(targetStringFromTarget({ project, target, configuration })); + }); + }); + }); + return targets; +} + +// TODO(jamesdaniels) memoize, dry up +export async function getContext(dir: string, targetOrConfiguration?: string) { + const [ + { NodeJsAsyncHost }, + { workspaces }, + { WorkspaceNodeModulesArchitectHost }, + { Architect, targetFromTargetString, targetStringFromTarget }, + { parse }, + ] = await Promise.all([ + relativeRequire(dir, "@angular-devkit/core/node"), + relativeRequire(dir, "@angular-devkit/core"), + relativeRequire(dir, "@angular-devkit/architect/node"), + relativeRequire(dir, "@angular-devkit/architect"), + relativeRequire(dir, "jsonc-parser"), + ]); + + const host = workspaces.createWorkspaceHost(new NodeJsAsyncHost()); + const { workspace } = await workspaces.readWorkspace(dir, host); + const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, dir); + const architect = new Architect(architectHost); + + let overrideTarget: Target | undefined; + let deployTarget: Target | undefined; + let project: string | undefined; + let buildTarget: Target | undefined; + let browserTarget: Target | undefined; + let serverTarget: Target | undefined; + let prerenderTarget: Target | undefined; + let serveTarget: Target | undefined; + let serveOptimizedImages = false; + + let configuration: string | undefined = undefined; + if (targetOrConfiguration) { + try { + overrideTarget = targetFromTargetString(targetOrConfiguration); + configuration = overrideTarget.configuration; + project = overrideTarget.project; + } catch (e) { + configuration = targetOrConfiguration; + } + } + + if (!project) { + const angularJson = parse(await host.readFile(join(dir, "angular.json"))); + project = angularJson.defaultProject; + } + + if (!project) { + const apps: string[] = []; + workspace.projects.forEach((value, key) => { + if (value.extensions.projectType === "application") apps.push(key); + }); + if (apps.length === 1) project = apps[0]; + } + + if (!project) + throw new FirebaseError( + "Unable to determine the application to deploy, specify a target via the FIREBASE_FRAMEWORKS_BUILD_TARGET environment variable", + ); + + const workspaceProject = workspace.projects.get(project); + if (!workspaceProject) throw new FirebaseError(`No project ${project} found.`); + + if (overrideTarget) { + const target = workspaceProject.targets.get(overrideTarget.target)!; + const builder = target.builder; + switch (builder) { + case ExpectedBuilder.DEPLOY: + deployTarget = overrideTarget; + break; + case ExpectedBuilder.APPLICATION: + buildTarget = overrideTarget; + break; + case ExpectedBuilder.BROWSER_ESBUILD: + case ExpectedBuilder.LEGACY_BROWSER: + browserTarget = overrideTarget; + break; + case ExpectedBuilder.LEGACY_DEVKIT_PRERENDER: + case ExpectedBuilder.LEGACY_NGUNIVERSAL_PRERENDER: + prerenderTarget = overrideTarget; + break; + case ExpectedBuilder.DEV_SERVER: + case ExpectedBuilder.LEGACY_NGUNIVERSAL_SSR_DEV_SERVER: + case ExpectedBuilder.LEGACY_DEVKIT_SSR_DEV_SERVER: + serveTarget = overrideTarget; + break; + default: + throw new FirebaseError(`builder ${builder} not known.`); + } + } else if (workspaceProject.targets.has("deploy")) { + const { builder, defaultConfiguration = "production" } = + workspaceProject.targets.get("deploy")!; + if (builder === ExpectedBuilder.DEPLOY) { + deployTarget = { + project, + target: "deploy", + configuration: configuration || defaultConfiguration, + }; + } + } + + if (deployTarget) { + const options = await architectHost + .getOptionsForTarget(deployTarget) + .catch(() => workspaceProject.targets.get(deployTarget!.target)?.options); + if (!options) throw new FirebaseError("Unable to get options for ng-deploy."); + if (options.buildTarget) { + assertIsString(options.buildTarget); + buildTarget = targetFromTargetString(options.buildTarget); + } + if (options.prerenderTarget) { + assertIsString(options.prerenderTarget); + prerenderTarget = targetFromTargetString(options.prerenderTarget); + } + if (options.browserTarget) { + assertIsString(options.browserTarget); + browserTarget = targetFromTargetString(options.browserTarget); + } + if (options.serverTarget) { + assertIsString(options.serverTarget); + serverTarget = targetFromTargetString(options.serverTarget); + } + if (options.serveTarget) { + assertIsString(options.serveTarget); + serveTarget = targetFromTargetString(options.serveTarget); + } + if (options.serveOptimizedImages) { + serveOptimizedImages = true; + } + if (prerenderTarget) { + const prerenderOptions = await architectHost.getOptionsForTarget(prerenderTarget); + if (!browserTarget) { + throw new FirebaseError("ng-deploy with prerenderTarget requires a browserTarget"); + } + if (targetStringFromTarget(browserTarget) !== prerenderOptions?.browserTarget) { + throw new FirebaseError( + "ng-deploy's browserTarget and prerender's browserTarget do not match. Please check your angular.json", + ); + } + if (serverTarget && targetStringFromTarget(serverTarget) !== prerenderOptions?.serverTarget) { + throw new FirebaseError( + "ng-deploy's serverTarget and prerender's serverTarget do not match. Please check your angular.json", + ); + } + if (!serverTarget) { + console.warn( + "Treating the application as fully rendered. Add a serverTarget to your deploy target in angular.json to utilize server-side rendering.", + ); + } + } + if (!buildTarget && !browserTarget) { + throw new FirebaseError( + "ng-deploy is missing a build target. Plase check your angular.json.", + ); + } + } else if (!overrideTarget) { + if (workspaceProject.targets.has("prerender")) { + const { defaultConfiguration = "production" } = workspaceProject.targets.get("prerender")!; + prerenderTarget = { + project, + target: "prerender", + configuration: configuration || defaultConfiguration, + }; + const options = await architectHost.getOptionsForTarget(prerenderTarget); + assertIsString(options?.browserTarget); + browserTarget = targetFromTargetString(options.browserTarget); + assertIsString(options?.serverTarget); + serverTarget = targetFromTargetString(options.serverTarget); + } + if (!buildTarget && !browserTarget && workspaceProject.targets.has("build")) { + const { builder, defaultConfiguration = "production" } = + workspaceProject.targets.get("build")!; + const target = { + project, + target: "build", + configuration: configuration || defaultConfiguration, + }; + if ( + builder === ExpectedBuilder.LEGACY_BROWSER || + builder === ExpectedBuilder.BROWSER_ESBUILD + ) { + browserTarget = target; + } else { + buildTarget = target; + } + } + if (!serverTarget && workspaceProject.targets.has("server")) { + const { defaultConfiguration = "production" } = workspaceProject.targets.get("server")!; + serverTarget = { + project, + target: "server", + configuration: configuration || defaultConfiguration, + }; + } + } + + if (!serveTarget) { + if (serverTarget && workspaceProject.targets.has("serve-ssr")) { + const { defaultConfiguration = "development" } = workspaceProject.targets.get("serve-ssr")!; + serveTarget = { + project, + target: "serve-ssr", + configuration: configuration || defaultConfiguration, + }; + } else if (workspaceProject.targets.has("serve")) { + const { defaultConfiguration = "development" } = workspaceProject.targets.get("serve")!; + serveTarget = { + project, + target: "serve", + configuration: configuration || defaultConfiguration, + }; + } + } + + for (const target of [ + deployTarget, + buildTarget, + prerenderTarget, + serverTarget, + browserTarget, + serveTarget, + ]) { + if (target) { + const targetString = targetStringFromTarget(target); + if (target.project !== project) + throw new FirebaseError( + `${targetString} is not in project ${project}. Please check your angular.json`, + ); + const definition = workspaceProject.targets.get(target.target); + if (!definition) throw new FirebaseError(`${target} could not be found in your angular.json`); + const { builder } = definition; + if (target === deployTarget && builder === ExpectedBuilder.DEPLOY) continue; + if (target === buildTarget && builder === ExpectedBuilder.APPLICATION) continue; + if (target === buildTarget && builder === ExpectedBuilder.LEGACY_BROWSER) continue; + if (target === browserTarget && builder === ExpectedBuilder.BROWSER_ESBUILD) continue; + if (target === browserTarget && builder === ExpectedBuilder.LEGACY_BROWSER) continue; + if (target === prerenderTarget && builder === ExpectedBuilder.LEGACY_DEVKIT_PRERENDER) + continue; + if (target === prerenderTarget && builder === ExpectedBuilder.LEGACY_NGUNIVERSAL_PRERENDER) + continue; + if (target === serverTarget && builder === ExpectedBuilder.LEGACY_SERVER) continue; + if (target === serveTarget && builder === ExpectedBuilder.LEGACY_NGUNIVERSAL_SSR_DEV_SERVER) + continue; + if (target === serveTarget && builder === ExpectedBuilder.LEGACY_DEVKIT_SSR_DEV_SERVER) + continue; + if (target === serveTarget && builder === ExpectedBuilder.DEV_SERVER) continue; + throw new FirebaseError( + `${definition.builder} (${targetString}) is not a recognized builder. Please check your angular.json`, + ); + } + } + + const buildOrBrowserTarget = buildTarget || browserTarget; + if (!buildOrBrowserTarget) { + throw new FirebaseError(`No build target on ${project}`); + } + const browserTargetOptions = await architectHost.getOptionsForTarget(buildOrBrowserTarget); + if (!browserTargetOptions) { + const targetString = targetStringFromTarget(buildOrBrowserTarget); + throw new FirebaseError(`Couldn't find options for ${targetString}.`); + } + + const baseHref = browserTargetOptions.baseHref || "/"; + assertIsString(baseHref); + + const buildTargetOptions = buildTarget && (await architectHost.getOptionsForTarget(buildTarget)); + const ssr = buildTarget ? !!buildTargetOptions?.ssr : !!serverTarget; + + return { + architect, + architectHost, + baseHref, + host, + buildTarget, + browserTarget, + prerenderTarget, + serverTarget, + serveTarget, + workspaceProject, + serveOptimizedImages, + ssr, + }; +} + +export async function getBrowserConfig(sourceDir: string, configuration: string) { + const { architectHost, browserTarget, buildTarget, baseHref, workspaceProject } = + await getContext(sourceDir, configuration); + const buildOrBrowserTarget = buildTarget || browserTarget; + if (!buildOrBrowserTarget) { + throw new AssertionError({ message: "expected build or browser target defined" }); + } + const [{ locales, defaultLocale }, targetOptions, builderName] = await Promise.all([ + localesForTarget(sourceDir, architectHost, buildOrBrowserTarget, workspaceProject), + architectHost.getOptionsForTarget(buildOrBrowserTarget), + architectHost.getBuilderNameForTarget(buildOrBrowserTarget), + ]); + + assertIsString(targetOptions?.outputPath); + + const outputPath = join( + targetOptions.outputPath, + buildTarget && builderName === ExpectedBuilder.APPLICATION ? "browser" : "", + ); + return { locales, baseHref, outputPath, defaultLocale }; +} + +export async function getServerConfig(sourceDir: string, configuration: string) { + const { + architectHost, + host, + buildTarget, + serverTarget, + browserTarget, + baseHref, + workspaceProject, + serveOptimizedImages, + ssr, + } = await getContext(sourceDir, configuration); + const buildOrBrowserTarget = buildTarget || browserTarget; + if (!buildOrBrowserTarget) { + throw new AssertionError({ message: "expected build or browser target to be defined" }); + } + const browserTargetOptions = await architectHost.getOptionsForTarget(buildOrBrowserTarget); + assertIsString(browserTargetOptions?.outputPath); + const browserOutputPath = join(browserTargetOptions.outputPath, buildTarget ? "browser" : "") + .split(sep) + .join(posix.sep); + const packageJson = JSON.parse(await host.readFile(join(sourceDir, "package.json"))); + + if (!ssr) { + return { + packageJson, + browserOutputPath, + serverOutputPath: undefined, + baseHref, + bundleDependencies: false, + externalDependencies: [], + serverLocales: [], + browserLocales: undefined, + defaultLocale: undefined, + serveOptimizedImages, + }; + } + const buildOrServerTarget = buildTarget || serverTarget; + if (!buildOrServerTarget) { + throw new AssertionError({ message: "expected build or server target to be defined" }); + } + const { locales: serverLocales, defaultLocale } = await localesForTarget( + sourceDir, + architectHost, + buildOrServerTarget, + workspaceProject, + ); + const serverTargetOptions = await architectHost.getOptionsForTarget(buildOrServerTarget); + assertIsString(serverTargetOptions?.outputPath); + const serverOutputPath = join(serverTargetOptions.outputPath, buildTarget ? "server" : "") + .split(sep) + .join(posix.sep); + if (serverLocales && !defaultLocale) { + throw new FirebaseError( + "It's required that your source locale to be one of the localize options", + ); + } + const serverEntry = buildTarget ? "server.mjs" : serverTarget && "main.js"; + const externalDependencies: string[] = (serverTargetOptions.externalDependencies as any) || []; + const bundleDependencies = serverTargetOptions.bundleDependencies ?? true; + const { locales: browserLocales } = await localesForTarget( + sourceDir, + architectHost, + buildOrBrowserTarget, + workspaceProject, + ); + return { + packageJson, + browserOutputPath, + serverOutputPath, + baseHref, + bundleDependencies, + externalDependencies, + serverLocales, + browserLocales, + defaultLocale, + serveOptimizedImages, + serverEntry, + }; +} + +export async function getBuildConfig(sourceDir: string, configuration: string) { + const { targetStringFromTarget } = await relativeRequire(sourceDir, "@angular-devkit/architect"); + const { + buildTarget, + browserTarget, + baseHref, + prerenderTarget, + serverTarget, + architectHost, + workspaceProject, + serveOptimizedImages, + ssr, + } = await getContext(sourceDir, configuration); + const targets = ( + buildTarget + ? [buildTarget] + : prerenderTarget + ? [prerenderTarget] + : [browserTarget, serverTarget].filter((it) => !!it) + ).map((it) => targetStringFromTarget(it!)); + const buildOrBrowserTarget = buildTarget || browserTarget; + if (!buildOrBrowserTarget) { + throw new AssertionError({ message: "expected build or browser target defined" }); + } + const locales = await localesForTarget( + sourceDir, + architectHost, + buildOrBrowserTarget, + workspaceProject, + ); + return { + targets, + baseHref, + locales, + serveOptimizedImages, + ssr, + }; +} + +/** + * Get Angular version in the following format: `major.minor.patch`, ignoring + * canary versions as it causes issues with semver comparisons. + */ +export function getAngularVersion(cwd: string): string | undefined { + const dependency = findDependency("@angular/core", { cwd, depth: 0, omitDev: false }); + if (!dependency) return undefined; + + const angularVersionSemver = coerce(dependency.version); + if (!angularVersionSemver) return dependency.version; + + return angularVersionSemver.toString(); +} diff --git a/src/frameworks/astro/index.spec.ts b/src/frameworks/astro/index.spec.ts new file mode 100644 index 00000000000..d0bc4c390b9 --- /dev/null +++ b/src/frameworks/astro/index.spec.ts @@ -0,0 +1,349 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { EventEmitter } from "events"; +import { Writable } from "stream"; +import * as crossSpawn from "cross-spawn"; +import * as fsExtra from "fs-extra"; + +import * as astroUtils from "./utils"; +import * as frameworkUtils from "../utils"; +import { + discover, + getDevModeHandle, + build, + ɵcodegenPublicDirectory, + ɵcodegenFunctionsDirectory, +} from "."; +import { FirebaseError } from "../../error"; +import { join } from "path"; + +describe("Astro", () => { + describe("discovery", () => { + const cwd = "."; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should find a static Astro app", async () => { + const publicDir = Math.random().toString(36).split(".")[1]; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(cwd) + .returns( + Promise.resolve({ + outDir: "dist", + publicDir, + output: "static", + adapter: undefined, + }), + ); + sandbox + .stub(frameworkUtils, "findDependency") + .withArgs("astro", { cwd, depth: 0, omitDev: false }) + .returns({ + version: "2.2.2", + resolved: "https://registry.npmjs.org/astro/-/astro-2.2.2.tgz", + overridden: false, + }); + expect(await discover(cwd)).to.deep.equal({ + mayWantBackend: false, + version: "2.2.2", + }); + }); + + it("should find an Astro SSR app", async () => { + const publicDir = Math.random().toString(36).split(".")[1]; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(cwd) + .returns( + Promise.resolve({ + outDir: "dist", + publicDir, + output: "server", + adapter: { + name: "@astrojs/node", + hooks: {}, + }, + }), + ); + sandbox + .stub(frameworkUtils, "findDependency") + .withArgs("astro", { cwd, depth: 0, omitDev: false }) + .returns({ + version: "2.2.2", + resolved: "https://registry.npmjs.org/astro/-/astro-2.2.2.tgz", + overridden: false, + }); + expect(await discover(cwd)).to.deep.equal({ + mayWantBackend: true, + version: "2.2.2", + }); + }); + }); + + describe("ɵcodegenPublicDirectory", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should copy over a static Astro app", async () => { + const root = Math.random().toString(36).split(".")[1]; + const dist = Math.random().toString(36).split(".")[1]; + const outDir = Math.random().toString(36).split(".")[1]; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(root) + .returns( + Promise.resolve({ + outDir, + publicDir: "xxx", + output: "static", + adapter: undefined, + }), + ); + + const copy = sandbox.stub(fsExtra, "copy"); + + await ɵcodegenPublicDirectory(root, dist); + expect(copy.getCalls().map((it) => it.args)).to.deep.equal([[join(root, outDir), dist]]); + }); + + it("should copy over an Astro SSR app", async () => { + const root = Math.random().toString(36).split(".")[1]; + const dist = Math.random().toString(36).split(".")[1]; + const outDir = Math.random().toString(36).split(".")[1]; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(root) + .returns( + Promise.resolve({ + outDir, + publicDir: "xxx", + output: "server", + adapter: { + name: "@astrojs/node", + hooks: {}, + }, + }), + ); + + const copy = sandbox.stub(fsExtra, "copy"); + + await ɵcodegenPublicDirectory(root, dist); + expect(copy.getCalls().map((it) => it.args)).to.deep.equal([ + [join(root, outDir, "client"), dist], + ]); + }); + }); + + describe("ɵcodegenFunctionsDirectory", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should copy over the cloud function", async () => { + const root = Math.random().toString(36).split(".")[1]; + const dist = Math.random().toString(36).split(".")[1]; + const outDir = Math.random().toString(36).split(".")[1]; + const packageJson = { a: Math.random().toString(36).split(".")[1] }; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(root) + .returns( + Promise.resolve({ + outDir, + publicDir: "xxx", + output: "server", + adapter: { + name: "@astrojs/node", + hooks: {}, + }, + }), + ); + sandbox + .stub(frameworkUtils, "readJSON") + .withArgs(join(root, "package.json")) + .returns(Promise.resolve(packageJson)); + + const copy = sandbox.stub(fsExtra, "copy"); + const bootstrapScript = astroUtils.getBootstrapScript(); + expect(await ɵcodegenFunctionsDirectory(root, dist)).to.deep.equal({ + packageJson, + bootstrapScript, + }); + expect(copy.getCalls().map((it) => it.args)).to.deep.equal([ + [join(root, outDir, "server"), dist], + ]); + }); + }); + + describe("build", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should build an Astro SSR app", async () => { + const process = new EventEmitter() as any; + process.stdin = new Writable(); + process.stdout = new EventEmitter(); + process.stderr = new EventEmitter(); + process.status = 0; + + const cwd = "."; + const publicDir = Math.random().toString(36).split(".")[1]; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(cwd) + .returns( + Promise.resolve({ + outDir: "dist", + publicDir, + output: "server", + adapter: { + name: "@astrojs/node", + hooks: {}, + }, + }), + ); + + const cli = Math.random().toString(36).split(".")[1]; + sandbox.stub(frameworkUtils, "getNodeModuleBin").withArgs("astro", cwd).returns(cli); + const stub = sandbox.stub(crossSpawn, "sync").returns(process); + + const result = build(cwd); + + process.emit("close"); + + expect(await result).to.deep.equal({ + wantsBackend: true, + }); + sinon.assert.calledWith(stub, cli, ["build"], { cwd, stdio: "inherit" }); + }); + + it("should fail to build an Astro SSR app w/wrong adapter", async () => { + const cwd = "."; + const publicDir = Math.random().toString(36).split(".")[1]; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(cwd) + .returns( + Promise.resolve({ + outDir: "dist", + publicDir, + output: "server", + adapter: { + name: "EPIC FAIL", + hooks: {}, + }, + }), + ); + + const cli = Math.random().toString(36).split(".")[1]; + sandbox.stub(frameworkUtils, "getNodeModuleBin").withArgs("astro", cwd).returns(cli); + + await expect(build(cwd)).to.eventually.rejectedWith( + FirebaseError, + "Deploying an Astro application with SSR on Firebase Hosting requires the @astrojs/node adapter in middleware mode. https://docs.astro.build/en/guides/integrations-guide/node/", + ); + }); + + it("should build an Astro static app", async () => { + const process = new EventEmitter() as any; + process.stdin = new Writable(); + process.stdout = new EventEmitter(); + process.stderr = new EventEmitter(); + process.status = 0; + + const cwd = "."; + const publicDir = Math.random().toString(36).split(".")[1]; + sandbox + .stub(astroUtils, "getConfig") + .withArgs(cwd) + .returns( + Promise.resolve({ + outDir: "dist", + publicDir, + output: "static", + adapter: undefined, + }), + ); + + const cli = Math.random().toString(36).split(".")[1]; + sandbox.stub(frameworkUtils, "getNodeModuleBin").withArgs("astro", cwd).returns(cli); + const stub = sandbox.stub(crossSpawn, "sync").returns(process); + + const result = build(cwd); + + process.emit("close"); + + expect(await result).to.deep.equal({ + wantsBackend: false, + }); + sinon.assert.calledWith(stub, cli, ["build"], { cwd, stdio: "inherit" }); + }); + }); + + describe("getDevModeHandle", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should resolve with dev server output", async () => { + const process = new EventEmitter() as any; + process.stdin = new Writable(); + process.stdout = new EventEmitter(); + process.stderr = new EventEmitter(); + process.status = 0; + + const cli = Math.random().toString(36).split(".")[1]; + sandbox.stub(frameworkUtils, "getNodeModuleBin").withArgs("astro", ".").returns(cli); + const stub = sandbox.stub(crossSpawn, "spawn").returns(process); + + const devModeHandle = getDevModeHandle("."); + + process.stdout.emit( + "data", + ` 🚀 astro v2.2.2 started in 64ms + + ┃ Local http://localhost:3000/ + ┃ Network use --host to expose + +`, + ); + + await expect(devModeHandle).eventually.be.fulfilled; + sinon.assert.calledWith(stub, cli, ["dev"], { cwd: "." }); + }); + }); +}); diff --git a/src/frameworks/astro/index.ts b/src/frameworks/astro/index.ts new file mode 100644 index 00000000000..6b7e7a5ace6 --- /dev/null +++ b/src/frameworks/astro/index.ts @@ -0,0 +1,74 @@ +import { sync as spawnSync, spawn } from "cross-spawn"; +import { copy, existsSync } from "fs-extra"; +import { join } from "path"; +import { BuildResult, Discovery, FrameworkType, SupportLevel } from "../interfaces"; +import { FirebaseError } from "../../error"; +import { readJSON, simpleProxy, warnIfCustomBuildScript, getNodeModuleBin } from "../utils"; +import { getAstroVersion, getBootstrapScript, getConfig } from "./utils"; + +export const name = "Astro"; +export const support = SupportLevel.Experimental; +export const type = FrameworkType.MetaFramework; +export const supportedRange = "2 - 4"; + +export async function discover(dir: string): Promise { + if (!existsSync(join(dir, "package.json"))) return; + const version = getAstroVersion(dir); + if (!version) return; + const { output } = await getConfig(dir); + return { + mayWantBackend: output !== "static", + version, + }; +} + +const DEFAULT_BUILD_SCRIPT = ["astro build"]; + +export async function build(cwd: string): Promise { + const cli = getNodeModuleBin("astro", cwd); + await warnIfCustomBuildScript(cwd, name, DEFAULT_BUILD_SCRIPT); + const { output, adapter } = await getConfig(cwd); + const wantsBackend = output !== "static"; + if (wantsBackend && adapter?.name !== "@astrojs/node") { + throw new FirebaseError( + "Deploying an Astro application with SSR on Firebase Hosting requires the @astrojs/node adapter in middleware mode. https://docs.astro.build/en/guides/integrations-guide/node/", + ); + } + const build = spawnSync(cli, ["build"], { cwd, stdio: "inherit" }); + if (build.status !== 0) throw new FirebaseError("Unable to build your Astro app"); + return { wantsBackend }; +} + +export async function ɵcodegenPublicDirectory(root: string, dest: string) { + const { outDir, output } = await getConfig(root); + // output: "server" in astro.config builds "client" and "server" folders, otherwise assets are in top-level outDir + const assetPath = join(root, outDir, output !== "static" ? "client" : ""); + await copy(assetPath, dest); +} + +export async function ɵcodegenFunctionsDirectory(sourceDir: string, destDir: string) { + const { outDir } = await getConfig(sourceDir); + const packageJson = await readJSON(join(sourceDir, "package.json")); + await copy(join(sourceDir, outDir, "server"), join(destDir)); + return { + packageJson, + bootstrapScript: getBootstrapScript(), + }; +} + +export async function getDevModeHandle(cwd: string) { + const host = new Promise((resolve, reject) => { + const cli = getNodeModuleBin("astro", cwd); + const serve = spawn(cli, ["dev"], { cwd }); + serve.stdout.on("data", (data: any) => { + process.stdout.write(data); + const match = data.toString().match(/(http:\/\/.+:\d+)/); + if (match) resolve(match[1]); + }); + serve.stderr.on("data", (data: any) => { + process.stderr.write(data); + }); + serve.on("exit", reject); + }); + return simpleProxy(await host); +} diff --git a/src/frameworks/astro/utils.ts b/src/frameworks/astro/utils.ts new file mode 100644 index 00000000000..a9abc411bac --- /dev/null +++ b/src/frameworks/astro/utils.ts @@ -0,0 +1,41 @@ +import { dirname, join, relative } from "path"; +import { findDependency } from "../utils"; +import { gte } from "semver"; +import { fileURLToPath } from "url"; + +const { dynamicImport } = require(true && "../../dynamicImport"); + +export function getBootstrapScript() { + // `astro build` with node adapter in middleware mode will generate a middleware at entry.mjs + // need to convert the export to `handle` to work with express integration + return `const entry = import('./entry.mjs');\nexport const handle = async (req, res) => (await entry).handler(req, res)`; +} + +export async function getConfig(cwd: string) { + const astroDirectory = dirname(require.resolve("astro/package.json", { paths: [cwd] })); + const version = getAstroVersion(cwd); + + let config; + const configPath = join(astroDirectory, "dist", "core", "config", "config.js"); + if (gte(version!, "2.9.7")) { + const { resolveConfig } = await dynamicImport(configPath); + const { astroConfig } = await resolveConfig({ root: cwd }, "build"); + config = astroConfig; + } else { + const { openConfig }: typeof import("astro/dist/core/config/config") = + await dynamicImport(configPath); + const logging: any = undefined; // TODO figure out the types here + const { astroConfig } = await openConfig({ cmd: "build", cwd, logging }); + config = astroConfig; + } + return { + outDir: relative(cwd, fileURLToPath(config.outDir)), + publicDir: relative(cwd, fileURLToPath(config.publicDir)), + output: config.output, + adapter: config.adapter, + }; +} + +export function getAstroVersion(cwd: string): string | undefined { + return findDependency("astro", { cwd, depth: 0, omitDev: false })?.version; +} diff --git a/src/frameworks/compose/discover/filesystem.spec.ts b/src/frameworks/compose/discover/filesystem.spec.ts new file mode 100644 index 00000000000..db1212b3393 --- /dev/null +++ b/src/frameworks/compose/discover/filesystem.spec.ts @@ -0,0 +1,56 @@ +import { MockFileSystem } from "./mockFileSystem"; +import { expect } from "chai"; + +describe("MockFileSystem", () => { + let fileSystem: MockFileSystem; + + before(() => { + fileSystem = new MockFileSystem({ + "package.json": JSON.stringify({ + name: "expressapp", + version: "1.0.0", + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + dependencies: { + express: "^4.18.2", + }, + }), + }); + }); + + describe("exists", () => { + it("should return true if file exists in the directory ", async () => { + const fileExists = await fileSystem.exists("package.json"); + + expect(fileExists).to.be.true; + expect(fileSystem.getExistsCache("package.json")).to.be.true; + }); + + it("should return false if file does not exist in the directory", async () => { + const fileExists = await fileSystem.exists("nonexistent.txt"); + + expect(fileExists).to.be.false; + }); + }); + + describe("read", () => { + it("should read and return the contents of the file", async () => { + const fileContent = await fileSystem.read("package.json"); + + const expected = JSON.stringify({ + name: "expressapp", + version: "1.0.0", + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + dependencies: { + express: "^4.18.2", + }, + }); + + expect(fileContent).to.equal(expected); + expect(fileSystem.getContentCache("package.json")).to.equal(expected); + }); + }); +}); diff --git a/src/frameworks/compose/discover/filesystem.ts b/src/frameworks/compose/discover/filesystem.ts new file mode 100644 index 00000000000..f1e7aa513d0 --- /dev/null +++ b/src/frameworks/compose/discover/filesystem.ts @@ -0,0 +1,55 @@ +import { FileSystem } from "./types"; +import { pathExists, readFile } from "fs-extra"; +import * as path from "path"; +import { FirebaseError } from "../../../error"; +import { logger } from "../../../logger"; + +/** + * Find files or read file contents present in the directory. + */ +export class LocalFileSystem implements FileSystem { + private readonly existsCache: Record = {}; + private readonly contentCache: Record = {}; + + constructor(private readonly cwd: string) {} + + async exists(file: string): Promise { + try { + if (!(file in this.contentCache)) { + this.existsCache[file] = await pathExists(path.resolve(this.cwd, file)); + } + + return this.existsCache[file]; + } catch (error) { + throw new FirebaseError(`Error occured while searching for file: ${error}`); + } + } + + async read(file: string): Promise { + try { + if (!(file in this.contentCache)) { + const fileContents = await readFile(path.resolve(this.cwd, file), "utf-8"); + this.contentCache[file] = fileContents; + } + return this.contentCache[file]; + } catch (error) { + logger.error("Error occured while reading file contents."); + throw error; + } + } +} + +/** + * Convert ENOENT errors into null + */ +export async function readOrNull(fs: FileSystem, path: string): Promise { + try { + return fs.read(path); + } catch (err: any) { + if (err && typeof err === "object" && err?.code === "ENOENT") { + logger.debug("ENOENT error occured while reading file."); + return null; + } + throw new Error(`Unknown error occured while reading file: ${err}`); + } +} diff --git a/src/frameworks/compose/discover/frameworkMatcher.spec.ts b/src/frameworks/compose/discover/frameworkMatcher.spec.ts new file mode 100644 index 00000000000..f4900a289eb --- /dev/null +++ b/src/frameworks/compose/discover/frameworkMatcher.spec.ts @@ -0,0 +1,171 @@ +import { MockFileSystem } from "./mockFileSystem"; +import { expect } from "chai"; +import { + frameworkMatcher, + removeEmbededFrameworks, + filterFrameworksWithFiles, + filterFrameworksWithDependencies, +} from "./frameworkMatcher"; +import { frameworkSpecs } from "./frameworkSpec"; +import { FrameworkSpec } from "./types"; + +describe("frameworkMatcher", () => { + let fileSystem: MockFileSystem; + const NODE_ID = "nodejs"; + + before(() => { + fileSystem = new MockFileSystem({ + "package.json": JSON.stringify({ + name: "expressapp", + version: "1.0.0", + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + dependencies: { + express: "^4.18.2", + }, + }), + "package-lock.json": "Unused: contents of package-lock file", + }); + }); + + describe("frameworkMatcher", () => { + it("should return express FrameworkSpec after analysing express application", async () => { + const expressDependency: Record = { + express: "^4.18.2", + }; + const matchedFramework = await frameworkMatcher( + NODE_ID, + fileSystem, + frameworkSpecs, + expressDependency, + ); + const expressFrameworkSpec: FrameworkSpec = { + id: "express", + runtime: "nodejs", + webFrameworkId: "Express.js", + requiredDependencies: [ + { + name: "express", + }, + ], + }; + + expect(matchedFramework).to.deep.equal(expressFrameworkSpec); + }); + }); + + describe("removeEmbededFrameworks", () => { + it("should return frameworks after removing embeded frameworks", () => { + const allFrameworks: FrameworkSpec[] = [ + { + id: "express", + runtime: "nodejs", + requiredDependencies: [], + }, + { + id: "next", + runtime: "nodejs", + requiredDependencies: [], + embedsFrameworks: ["react"], + }, + { + id: "react", + runtime: "nodejs", + requiredDependencies: [], + }, + ]; + const actual = removeEmbededFrameworks(allFrameworks); + const expected: FrameworkSpec[] = [ + { + id: "express", + runtime: "nodejs", + requiredDependencies: [], + }, + { + id: "next", + runtime: "nodejs", + requiredDependencies: [], + embedsFrameworks: ["react"], + }, + ]; + + expect(actual).to.have.deep.members(expected); + expect(actual).to.have.length(2); + }); + }); + + describe("filterFrameworksWithFiles", () => { + it("should return frameworks having all the required files", async () => { + const allFrameworks: FrameworkSpec[] = [ + { + id: "express", + runtime: "nodejs", + requiredDependencies: [], + requiredFiles: [["package.json", "package-lock.json"]], + }, + { + id: "next", + runtime: "nodejs", + requiredDependencies: [], + requiredFiles: [["next.config.js"], "next.config.ts"], + }, + ]; + const actual = await filterFrameworksWithFiles(allFrameworks, fileSystem); + const expected: FrameworkSpec[] = [ + { + id: "express", + runtime: "nodejs", + requiredDependencies: [], + requiredFiles: [["package.json", "package-lock.json"]], + }, + ]; + + expect(actual).to.have.deep.members(expected); + expect(actual).to.have.length(1); + }); + }); + + describe("filterFrameworksWithDependencies", () => { + it("should return frameworks having required dependencies with in the project dependencies", () => { + const allFrameworks: FrameworkSpec[] = [ + { + id: "express", + runtime: "nodejs", + requiredDependencies: [ + { + name: "express", + }, + ], + }, + { + id: "next", + runtime: "nodejs", + requiredDependencies: [ + { + name: "next", + }, + ], + }, + ]; + const projectDependencies: Record = { + express: "^4.18.2", + }; + const actual = filterFrameworksWithDependencies(allFrameworks, projectDependencies); + const expected: FrameworkSpec[] = [ + { + id: "express", + runtime: "nodejs", + requiredDependencies: [ + { + name: "express", + }, + ], + }, + ]; + + expect(actual).to.have.deep.members(expected); + expect(actual).to.have.length(1); + }); + }); +}); diff --git a/src/frameworks/compose/discover/frameworkMatcher.ts b/src/frameworks/compose/discover/frameworkMatcher.ts new file mode 100644 index 00000000000..cf11b65b5d3 --- /dev/null +++ b/src/frameworks/compose/discover/frameworkMatcher.ts @@ -0,0 +1,109 @@ +import { FirebaseError } from "../../../error"; +import { FrameworkSpec, FileSystem } from "./types"; +import { logger } from "../../../logger"; + +/** + * + */ +export function filterFrameworksWithDependencies( + allFrameworkSpecs: FrameworkSpec[], + dependencies: Record, +): FrameworkSpec[] { + return allFrameworkSpecs.filter((framework) => { + return framework.requiredDependencies.every((dependency) => { + return dependency.name in dependencies; + }); + }); +} + +/** + * + */ +export async function filterFrameworksWithFiles( + allFrameworkSpecs: FrameworkSpec[], + fs: FileSystem, +): Promise { + try { + const filteredFrameworks = []; + for (const framework of allFrameworkSpecs) { + if (!framework.requiredFiles) { + filteredFrameworks.push(framework); + continue; + } + let isRequired = true; + for (let files of framework.requiredFiles) { + files = Array.isArray(files) ? files : [files]; + for (const file of files) { + isRequired = isRequired && (await fs.exists(file)); + if (!isRequired) { + break; + } + } + } + if (isRequired) { + filteredFrameworks.push(framework); + } + } + + return filteredFrameworks; + } catch (error) { + logger.error("Error: Unable to filter frameworks based on required files", error); + throw error; + } +} + +/** + * Embeded frameworks help to resolve tiebreakers when multiple frameworks are discovered. + * Ex: "next" embeds "react", so if both frameworks are discovered, + * we can suggest "next" commands by removing its embeded framework (react). + */ +export function removeEmbededFrameworks(allFrameworkSpecs: FrameworkSpec[]): FrameworkSpec[] { + const embededFrameworkSet: Set = new Set(); + + for (const framework of allFrameworkSpecs) { + if (!framework.embedsFrameworks) { + continue; + } + for (const item of framework.embedsFrameworks) { + embededFrameworkSet.add(item); + } + } + + return allFrameworkSpecs.filter((item) => !embededFrameworkSet.has(item.id)); +} + +/** + * Identifies the best FrameworkSpec for the codebase. + */ +export async function frameworkMatcher( + runtime: string, + fs: FileSystem, + frameworks: FrameworkSpec[], + dependencies: Record, +): Promise { + try { + const filterRuntimeFramework = frameworks.filter((framework) => framework.runtime === runtime); + const frameworksWithDependencies = filterFrameworksWithDependencies( + filterRuntimeFramework, + dependencies, + ); + const frameworkWithFiles = await filterFrameworksWithFiles(frameworksWithDependencies, fs); + const allMatches = removeEmbededFrameworks(frameworkWithFiles); + + if (allMatches.length === 0) { + return null; + } + if (allMatches.length > 1) { + const frameworkNames = allMatches.map((framework) => framework.id); + throw new FirebaseError( + `Multiple Frameworks are matched: ${frameworkNames.join( + ", ", + )} Manually set up override commands in firebase.json`, + ); + } + + return allMatches[0]; + } catch (error: any) { + throw new FirebaseError(`Failed to match the correct framework: ${error}`); + } +} diff --git a/src/frameworks/compose/discover/frameworkSpec.ts b/src/frameworks/compose/discover/frameworkSpec.ts new file mode 100644 index 00000000000..70655a85b07 --- /dev/null +++ b/src/frameworks/compose/discover/frameworkSpec.ts @@ -0,0 +1,38 @@ +import { FrameworkSpec } from "./types"; + +export const frameworkSpecs: FrameworkSpec[] = [ + { + id: "express", + runtime: "nodejs", + webFrameworkId: "Express.js", + requiredDependencies: [ + { + name: "express", + }, + ], + }, + { + id: "nextjs", + runtime: "nodejs", + webFrameworkId: "Next.js", + requiredFiles: ["next.config.js", "next.config.ts"], + requiredDependencies: [ + { + name: "next", + }, + ], + commands: { + build: { + cmd: "next build", + }, + dev: { + cmd: "next dev", + env: { NODE_ENV: "dev" }, + }, + run: { + cmd: "next run", + env: { NODE_ENV: "production" }, + }, + }, + }, +]; diff --git a/src/frameworks/compose/discover/index.ts b/src/frameworks/compose/discover/index.ts new file mode 100644 index 00000000000..ed9937f07ea --- /dev/null +++ b/src/frameworks/compose/discover/index.ts @@ -0,0 +1,43 @@ +import { Runtime, FileSystem, FrameworkSpec, RuntimeSpec } from "./types"; +import { NodejsRuntime } from "./runtime/node"; +import { FirebaseError } from "../../../error"; + +const supportedRuntimes: Runtime[] = [new NodejsRuntime()]; + +/** + * Discover the best matching runtime specs for the application. + */ +export async function discover( + fs: FileSystem, + allFrameworkSpecs: FrameworkSpec[], +): Promise { + try { + let discoveredRuntime = undefined; + for (const runtime of supportedRuntimes) { + if (await runtime.match(fs)) { + if (!discoveredRuntime) { + discoveredRuntime = runtime; + } else { + throw new FirebaseError( + `Conflit occurred as multiple runtimes ${discoveredRuntime.getRuntimeName()}, ${runtime.getRuntimeName()} are discovered in the application.`, + ); + } + } + } + + if (!discoveredRuntime) { + throw new FirebaseError( + `Unable to determine the specific runtime for the application. The supported runtime options include ${supportedRuntimes + .map((x) => x.getRuntimeName()) + .join(" , ")}.`, + ); + } + const runtimeSpec = await discoveredRuntime.analyseCodebase(fs, allFrameworkSpecs); + + return runtimeSpec; + } catch (error: any) { + throw new FirebaseError( + `Failed to identify required specifications to execute the application: ${error}`, + ); + } +} diff --git a/src/frameworks/compose/discover/mockFileSystem.ts b/src/frameworks/compose/discover/mockFileSystem.ts new file mode 100644 index 00000000000..44264105dfe --- /dev/null +++ b/src/frameworks/compose/discover/mockFileSystem.ts @@ -0,0 +1,38 @@ +import { FileSystem } from "./types"; + +export class MockFileSystem implements FileSystem { + private readonly existsCache: Record = {}; + private readonly contentCache: Record = {}; + + constructor(private readonly fileSys: Record) {} + + exists(path: string): Promise { + if (!(path in this.existsCache)) { + this.existsCache[path] = path in this.fileSys; + } + + return Promise.resolve(this.existsCache[path]); + } + + read(path: string): Promise { + if (!(path in this.contentCache)) { + if (!(path in this.fileSys)) { + const err = new Error("File path not found"); + err.cause = "ENOENT"; + throw err; + } else { + this.contentCache[path] = this.fileSys[path]; + } + } + + return Promise.resolve(this.contentCache[path]); + } + + getContentCache(path: string): string { + return this.contentCache[path]; + } + + getExistsCache(path: string): boolean { + return this.existsCache[path]; + } +} diff --git a/src/frameworks/compose/discover/runtime/node.spec.ts b/src/frameworks/compose/discover/runtime/node.spec.ts new file mode 100644 index 00000000000..09cb26e9649 --- /dev/null +++ b/src/frameworks/compose/discover/runtime/node.spec.ts @@ -0,0 +1,238 @@ +import { MockFileSystem } from "../mockFileSystem"; +import { expect } from "chai"; +import { NodejsRuntime, PackageJSON } from "./node"; +import { FrameworkSpec } from "../types"; +import { FirebaseError } from "../../../../error"; + +describe("NodejsRuntime", () => { + let nodeJSRuntime: NodejsRuntime; + let allFrameworks: FrameworkSpec[]; + + before(() => { + nodeJSRuntime = new NodejsRuntime(); + allFrameworks = [ + { + id: "express", + runtime: "nodejs", + requiredDependencies: [{ name: "express" }], + }, + { + id: "next", + runtime: "nodejs", + requiredDependencies: [{ name: "next" }], + requiredFiles: [["next.config.js"], "next.config.ts"], + embedsFrameworks: ["react"], + commands: { + dev: { + cmd: "next dev", + env: { NODE_ENV: "dev" }, + }, + }, + }, + ]; + }); + + describe("getNodeImage", () => { + it("should return a valid node Image", () => { + const version: Record = { + node: "18", + }; + const actualImage = nodeJSRuntime.getNodeImage(version); + const expectedImage = "us-docker.pkg.dev/firestack-build/test/run"; + + expect(actualImage).to.deep.equal(expectedImage); + }); + }); + + describe("getPackageManager", () => { + it("should return yarn package manager", async () => { + const fileSystem = new MockFileSystem({ + "yarn.lock": "It is test file", + }); + const actual = await nodeJSRuntime.getPackageManager(fileSystem); + const expected = "yarn"; + + expect(actual).to.equal(expected); + }); + }); + + describe("getDependencies", () => { + it("should return direct and transitive dependencies", () => { + const packageJSON: PackageJSON = { + dependencies: { + express: "^4.18.2", + }, + devDependencies: { + nodemon: "^2.0.12", + mocha: "^9.1.1", + }, + }; + const actual = nodeJSRuntime.getDependencies(packageJSON); + const expected = { + express: "^4.18.2", + nodemon: "^2.0.12", + mocha: "^9.1.1", + }; + + expect(actual).to.deep.equal(expected); + }); + }); + + describe("detectedCommands", () => { + it("should prepend npx to framework commands", async () => { + const fs = new MockFileSystem({ + "package.json": "Test file", + }); + const matchedFramework: FrameworkSpec = { + id: "next", + runtime: "nodejs", + requiredDependencies: [], + commands: { + dev: { + cmd: "next dev", + env: { NODE_ENV: "dev" }, + }, + }, + }; + const scripts = { + build: "next build", + start: "next start", + }; + + const actual = await nodeJSRuntime.detectedCommands("yarn", scripts, matchedFramework, fs); + const expected = { + build: { + cmd: "yarn run build", + }, + dev: { + cmd: "npx next dev", + env: { NODE_ENV: "dev" }, + }, + run: { + cmd: "yarn run start", + env: { NODE_ENV: "production" }, + }, + }; + + expect(actual).to.deep.equal(expected); + }); + + it("should prefer scripts over framework commands", async () => { + const fs = new MockFileSystem({ + "package.json": "Test file", + }); + const matchedFramework: FrameworkSpec = { + id: "next", + runtime: "nodejs", + requiredDependencies: [], + commands: { + build: { + cmd: "next build testing", + }, + run: { + cmd: "next start testing", + env: { NODE_ENV: "production" }, + }, + dev: { + cmd: "next dev", + env: { NODE_ENV: "dev" }, + }, + }, + }; + const scripts = { + build: "next build", + start: "next start", + }; + + const actual = await nodeJSRuntime.detectedCommands("yarn", scripts, matchedFramework, fs); + const expected = { + build: { + cmd: "yarn run build", + }, + dev: { + cmd: "npx next dev", + env: { NODE_ENV: "dev" }, + }, + run: { + cmd: "yarn run start", + env: { NODE_ENV: "production" }, + }, + }; + + expect(actual).to.deep.equal(expected); + }); + }); + + describe("analyseCodebase", () => { + it("should return runtime specs", async () => { + const fileSystem = new MockFileSystem({ + "next.config.js": "For testing", + "next.config.ts": "For testing", + "package.json": JSON.stringify({ + scripts: { + build: "next build", + start: "next start", + }, + dependencies: { + next: "13.4.5", + react: "18.2.0", + }, + engines: { + node: "18", + }, + }), + }); + + const actual = await nodeJSRuntime.analyseCodebase(fileSystem, allFrameworks); + const expected = { + id: "nodejs", + baseImage: "us-docker.pkg.dev/firestack-build/test/run", + packageManagerInstallCommand: undefined, + installCommand: "npm install", + detectedCommands: { + build: { + cmd: "npm run build", + }, + dev: { + cmd: "npx next dev", + env: { NODE_ENV: "dev" }, + }, + run: { + cmd: "npm run start", + env: { NODE_ENV: "production" }, + }, + }, + }; + + expect(actual).to.deep.equal(expected); + }); + + it("should return error", async () => { + const fileSystem = new MockFileSystem({ + "next.config.js": "For testing purpose.", + "next.config.ts": "For testing purpose.", + "package.json": JSON.stringify({ + scripts: { + build: "next build", + start: "next start", + }, + dependencies: { + // Having both express and next as dependencies. + express: "2.0.8", + next: "13.4.5", + react: "18.2.0", + }, + engines: { + node: "18", + }, + }), + }); + + // Failed with multiple framework matches + await expect(nodeJSRuntime.analyseCodebase(fileSystem, allFrameworks)).to.be.rejectedWith( + FirebaseError, + "Failed to parse engine", + ); + }); + }); +}); diff --git a/src/frameworks/compose/discover/runtime/node.ts b/src/frameworks/compose/discover/runtime/node.ts new file mode 100644 index 00000000000..780ca2cb227 --- /dev/null +++ b/src/frameworks/compose/discover/runtime/node.ts @@ -0,0 +1,211 @@ +import { readOrNull } from "../filesystem"; +import { FileSystem, FrameworkSpec, Runtime } from "../types"; +import { RuntimeSpec } from "../types"; +import { frameworkMatcher } from "../frameworkMatcher"; +import { LifecycleCommands } from "../types"; +import { Command } from "../types"; +import { FirebaseError } from "../../../../error"; +import { logger } from "../../../../logger"; +import { conjoinOptions } from "../../../utils"; + +export interface PackageJSON { + dependencies?: Record; + devDependencies?: Record; + scripts?: Record; + engines?: Record; +} +type PackageManager = "npm" | "yarn"; + +const supportedNodeVersions: string[] = ["18"]; +const NODE_RUNTIME_ID = "nodejs"; +const PACKAGE_JSON = "package.json"; +const YARN_LOCK = "yarn.lock"; + +export class NodejsRuntime implements Runtime { + private readonly runtimeRequiredFiles: string[] = [PACKAGE_JSON]; + + // Checks if the codebase is using Node as runtime. + async match(fs: FileSystem): Promise { + const areAllFilesPresent = await Promise.all( + this.runtimeRequiredFiles.map((file) => fs.exists(file)), + ); + + return areAllFilesPresent.every((present) => present); + } + + getRuntimeName(): string { + return NODE_RUNTIME_ID; + } + + getNodeImage(engine: Record | undefined): string { + // If no version is mentioned explicitly, assuming application is compatible with latest version. + if (!engine || !engine.node) { + return "us-docker.pkg.dev/firestack-build/test/run"; + } + const versionNumber = engine.node; + + if (!supportedNodeVersions.includes(versionNumber)) { + throw new FirebaseError( + `This integration expects Node version ${conjoinOptions( + supportedNodeVersions, + "or", + )}. You're running version ${versionNumber}, which is not compatible.`, + ); + } + + return "us-docker.pkg.dev/firestack-build/test/run"; + } + + async getPackageManager(fs: FileSystem): Promise { + try { + if (await fs.exists(YARN_LOCK)) { + return "yarn"; + } + + return "npm"; + } catch (error: any) { + logger.error("Failed to check files to identify package manager"); + throw error; + } + } + + getDependencies(packageJSON: PackageJSON): Record { + return { ...packageJSON.dependencies, ...packageJSON.devDependencies }; + } + + packageManagerInstallCommand(packageManager: PackageManager): string | undefined { + const packages: string[] = []; + if (packageManager === "yarn") { + packages.push("yarn"); + } + if (!packages.length) { + return undefined; + } + + return `npm install --global ${packages.join(" ")}`; + } + + installCommand(fs: FileSystem, packageManager: PackageManager): string { + let installCmd = "npm install"; + + if (packageManager === "yarn") { + installCmd = "yarn install"; + } + + return installCmd; + } + + async detectedCommands( + packageManager: PackageManager, + scripts: Record | undefined, + matchedFramework: FrameworkSpec | null, + fs: FileSystem, + ): Promise { + return { + build: this.getBuildCommand(packageManager, scripts, matchedFramework), + dev: this.getDevCommand(packageManager, scripts, matchedFramework), + run: await this.getRunCommand(packageManager, scripts, matchedFramework, fs), + }; + } + + executeScript(packageManager: string, scriptName: string): string { + return `${packageManager} run ${scriptName}`; + } + + executeFrameworkCommand(packageManager: PackageManager, command: Command): Command { + if (packageManager === "npm" || packageManager === "yarn") { + command.cmd = "npx " + command.cmd; + } + + return command; + } + + getBuildCommand( + packageManager: PackageManager, + scripts: Record | undefined, + matchedFramework: FrameworkSpec | null, + ): Command | undefined { + let buildCommand: Command = { cmd: "" }; + if (scripts?.build) { + buildCommand.cmd = this.executeScript(packageManager, "build"); + } else if (matchedFramework && matchedFramework.commands?.build) { + buildCommand = matchedFramework.commands.build; + buildCommand = this.executeFrameworkCommand(packageManager, buildCommand); + } + + return buildCommand.cmd === "" ? undefined : buildCommand; + } + + getDevCommand( + packageManager: PackageManager, + scripts: Record | undefined, + matchedFramework: FrameworkSpec | null, + ): Command | undefined { + let devCommand: Command = { cmd: "", env: { NODE_ENV: "dev" } }; + if (scripts?.dev) { + devCommand.cmd = this.executeScript(packageManager, "dev"); + } else if (matchedFramework && matchedFramework.commands?.dev) { + devCommand = matchedFramework.commands.dev; + devCommand = this.executeFrameworkCommand(packageManager, devCommand); + } + + return devCommand.cmd === "" ? undefined : devCommand; + } + + async getRunCommand( + packageManager: PackageManager, + scripts: Record | undefined, + matchedFramework: FrameworkSpec | null, + fs: FileSystem, + ): Promise { + let runCommand: Command = { cmd: "", env: { NODE_ENV: "production" } }; + if (scripts?.start) { + runCommand.cmd = this.executeScript(packageManager, "start"); + } else if (matchedFramework && matchedFramework.commands?.run) { + runCommand = matchedFramework.commands.run; + runCommand = this.executeFrameworkCommand(packageManager, runCommand); + } else if (scripts?.main) { + runCommand.cmd = `node ${scripts.main}`; + } else if (await fs.exists("index.js")) { + runCommand.cmd = `node index.js`; + } + + return runCommand.cmd === "" ? undefined : runCommand; + } + + async analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise { + try { + const packageJSONRaw = await readOrNull(fs, PACKAGE_JSON); + let packageJSON: PackageJSON = {}; + if (packageJSONRaw) { + packageJSON = JSON.parse(packageJSONRaw) as PackageJSON; + } + const packageManager = await this.getPackageManager(fs); + const nodeImage = this.getNodeImage(packageJSON.engines); + const dependencies = this.getDependencies(packageJSON); + const matchedFramework = await frameworkMatcher( + NODE_RUNTIME_ID, + fs, + allFrameworkSpecs, + dependencies, + ); + + const runtimeSpec: RuntimeSpec = { + id: NODE_RUNTIME_ID, + baseImage: nodeImage, + packageManagerInstallCommand: this.packageManagerInstallCommand(packageManager), + installCommand: this.installCommand(fs, packageManager), + detectedCommands: await this.detectedCommands( + packageManager, + packageJSON.scripts, + matchedFramework, + fs, + ), + }; + + return runtimeSpec; + } catch (error: any) { + throw new FirebaseError(`Failed to parse engine: ${error}`); + } + } +} diff --git a/src/frameworks/compose/discover/types.ts b/src/frameworks/compose/discover/types.ts new file mode 100644 index 00000000000..a89fb00c312 --- /dev/null +++ b/src/frameworks/compose/discover/types.ts @@ -0,0 +1,97 @@ +import { AppBundle } from "../interfaces"; + +export interface FileSystem { + exists(file: string): Promise; + read(file: string): Promise; +} + +export interface Runtime { + match(fs: FileSystem): Promise; + getRuntimeName(): string; + analyseCodebase(fs: FileSystem, allFrameworkSpecs: FrameworkSpec[]): Promise; +} + +export interface Command { + // Consider: string[] for series of commands that must execute successfully + // in sequence. + cmd: string; + + // Environment in which command is executed. + env?: Record; +} + +export interface LifecycleCommands { + build?: Command; + run?: Command; + dev?: Command; +} + +export interface FrameworkSpec { + id: string; + + // Only analyze Frameworks with a runtime that matches the matched runtime + runtime: string; + + // e.g. nextjs. Used to verify that Web Frameworks' legacy code and the + // FrameworkSpec agree with one another + webFrameworkId?: string; + + // List of dependencies that should be present in the project. + requiredDependencies: Array<{ + name: string; + // Version + semver?: string; + }>; + + // If a requiredFiles is an array, then one of the files in the array must match. + // This supports, for example, a file that can be a js, ts, or mjs file. + requiredFiles?: Array; + + // Any commands that this framework needs that are not standard for the + // runtime. Often times, this can be empty (e.g. depend on npm run build and + // npm run start) + commands?: LifecycleCommands; + + // We must resolve to a single framework when getting build/dev/run commands. + // embedsFrameworks helps decide tiebreakers by saying, for example, that "astro" + // can embed "svelte", so if both frameworks are discovered, monospace can + // suggest both frameworks' plugins, but should run astro's commands. + embedsFrameworks?: string[]; +} + +export interface RuntimeSpec { + // e.g. `nodejs` + id: string; + + // e.g. `node18-slim`. Depends on user code (e.g. engine field in package.json) + baseImage: string; + + // e.g. `npm install yarn typescript` + packageManagerInstallCommand?: string; + + // e.g. `npm ci`, `npm install`, `yarn` + installCommand?: string; + + // Commands to run right before exporting the container image + // e.g. npm prune --omit=dev, yarn install --production=true + exportCommands?: string[]; + + // The runtime has detected a command that should always be run irrespective of + // the framework (e.g. the "build" script always wins in Node) + detectedCommands?: LifecycleCommands; + + environmentVariables?: Record; + + // Framework authors can execute framework-specific code using hooks at different stages of Frameworks API build process. + frameworkHooks?: FrameworkHooks; +} + +export interface FrameworkHooks { + // Programmatic hook with access to filesystem and nodejs API to inspect the workspace. + // Primarily intended to gather hints relevant to the build. + afterInstall?: (b: AppBundle) => AppBundle; + + // Programmatic hook with access to filesystem and nodejs API to inspect the build artifacts. + // Primarily intended to informs what assets should be deployed. + afterBuild?: (b: AppBundle) => AppBundle; +} diff --git a/src/frameworks/compose/driver/docker.spec.ts b/src/frameworks/compose/driver/docker.spec.ts new file mode 100644 index 00000000000..141ac8bd9d2 --- /dev/null +++ b/src/frameworks/compose/driver/docker.spec.ts @@ -0,0 +1,92 @@ +import { expect } from "chai"; +import { DockerfileBuilder } from "./docker"; + +describe("DockerfileBuilder", () => { + describe("from", () => { + it("should add a FROM instruction to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.from("node:18", "base"); + expect(builder.toString()).to.equal("FROM node:18 AS base\n"); + }); + + it("should add a FROM instruction to the Dockerfile without a name", () => { + const builder = new DockerfileBuilder(); + builder.from("node:18"); + expect(builder.toString()).to.equal("FROM node:18\n"); + }); + }); + + describe("fromLastStage", () => { + it("should add a FROM instruction to the Dockerfile using the last stage name", () => { + const builder = new DockerfileBuilder(); + builder.from("node:18", "base").fromLastStage("test"); + expect(builder.toString()).to.equal("FROM node:18 AS base\nFROM base AS test\n"); + }); + }); + + describe("tempFrom", () => { + it("should add a FROM instruction without updating last stage", () => { + const builder = new DockerfileBuilder(); + builder.from("node:18", "base").tempFrom("node:20", "temp").fromLastStage("test"); + expect(builder.toString()).to.equal( + "FROM node:18 AS base\nFROM node:20 AS temp\nFROM base AS test\n", + ); + }); + }); + + describe("workdir", () => { + it("should add a WORKDIR instruction to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.workdir("/app"); + expect(builder.toString()).to.equal("WORKDIR /app\n"); + }); + }); + + describe("run", () => { + it("should add a RUN instruction to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.run('echo "test"'); + expect(builder.toString()).to.equal('RUN echo "test"\n'); + }); + }); + + describe("cmd", () => { + it("should add a CMD instruction to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.cmd(["node", "index.js"]); + expect(builder.toString()).to.equal('CMD ["node", "index.js"]\n'); + }); + }); + + describe("copyForFirebase", () => { + it("should add a COPY instruction to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.copyForFirebase("src", "dest"); + expect(builder.toString()).to.equal("COPY --chown=firebase:firebase src dest\n"); + }); + }); + + describe("copyFrom", () => { + it("should add a COPY instruction to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.copyFrom("src", "dest", "stage"); + expect(builder.toString()).to.equal("COPY --from=stage src dest\n"); + }); + }); + + describe("env", () => { + it("should add an ENV instruction to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.env("NODE_ENV", "production"); + expect(builder.toString()).to.equal('ENV NODE_ENV="production"\n'); + }); + }); + + describe("envs", () => { + it("should add multiple ENV instructions to the Dockerfile", () => { + const builder = new DockerfileBuilder(); + builder.envs({ NODE_ENV: "production", PORT: "8080" }); + expect(builder.toString()).to.equal('ENV NODE_ENV="production"\nENV PORT="8080"\n'); + }); + }); +}); diff --git a/src/frameworks/compose/driver/docker.ts b/src/frameworks/compose/driver/docker.ts new file mode 100644 index 00000000000..f5a13f36ba5 --- /dev/null +++ b/src/frameworks/compose/driver/docker.ts @@ -0,0 +1,221 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as spawn from "cross-spawn"; + +import { AppBundle, Driver, Hook } from "../interfaces"; +import { BUNDLE_PATH, genHookScript } from "./hooks"; +import { RuntimeSpec } from "../discover/types"; + +const ADAPTER_SCRIPTS_PATH = "./.firebase/adapters" as const; + +const DOCKER_STAGE_INSTALL = "installer" as const; +const DOCKER_STAGE_BUILD = "builder" as const; + +export class DockerfileBuilder { + private dockerfile = ""; + private lastStage = ""; + + from(image: string, name?: string): DockerfileBuilder { + this.dockerfile += `FROM ${image}`; + if (name) { + this.dockerfile += ` AS ${name}`; + this.lastStage = name; + } + this.dockerfile += "\n"; + return this; + } + + fromLastStage(name: string): DockerfileBuilder { + return this.from(this.lastStage, name); + } + + /** + * Last `from` but does not update the lastStage. + */ + tempFrom(image: string, name?: string): DockerfileBuilder { + this.dockerfile += `FROM ${image}`; + if (name) { + this.dockerfile += ` AS ${name}`; + } + this.dockerfile += "\n"; + return this; + } + + workdir(dir: string): DockerfileBuilder { + this.dockerfile += `WORKDIR ${dir}\n`; + return this; + } + + copyForFirebase(src: string, dest: string, from?: string): DockerfileBuilder { + if (from) { + this.dockerfile += `COPY --chown=firebase:firebase --from=${from} ${src} ${dest}\n`; + } else { + this.dockerfile += `COPY --chown=firebase:firebase ${src} ${dest}\n`; + } + return this; + } + + copyFrom(src: string, dest: string, from: string) { + this.dockerfile += `COPY --from=${from} ${src} ${dest}\n`; + return this; + } + + run(cmd: string, mount?: string): DockerfileBuilder { + if (mount) { + this.dockerfile += `RUN --mount=${mount} ${cmd}\n`; + } else { + this.dockerfile += `RUN ${cmd}\n`; + } + return this; + } + + env(key: string, value: string): DockerfileBuilder { + this.dockerfile += `ENV ${key}="${value}"\n`; + return this; + } + + envs(envs: Record): DockerfileBuilder { + for (const [key, value] of Object.entries(envs)) { + this.env(key, value); + } + return this; + } + + cmd(cmds: string[]): DockerfileBuilder { + this.dockerfile += `CMD [${cmds.map((c) => `"${c}"`).join(", ")}]\n`; + return this; + } + + user(user: string): DockerfileBuilder { + this.dockerfile += `USER ${user}\n`; + return this; + } + + toString(): string { + return this.dockerfile; + } +} + +export class DockerDriver implements Driver { + private dockerfileBuilder; + + constructor(readonly spec: RuntimeSpec) { + this.dockerfileBuilder = new DockerfileBuilder(); + this.dockerfileBuilder.from(spec.baseImage, "base").user("firebase"); + } + + private execDockerPush(args: string[]) { + console.debug(JSON.stringify({ message: `executing docker build: ${args.join(" ")}` })); + console.info( + JSON.stringify({ foo: "bar", message: `executing docker build: ${args.join(" ")}` }), + ); + console.error(JSON.stringify({ message: `executing docker build: ${args.join(" ")}` })); + return spawn.sync("docker", ["push", ...args], { + stdio: [/* stdin= */ "pipe", /* stdout= */ "inherit", /* stderr= */ "inherit"], + }); + } + + private execDockerBuild(args: string[], contextDir: string) { + console.log(`executing docker build: ${args.join(" ")} ${contextDir}`); + console.log(this.dockerfileBuilder.toString()); + return spawn.sync("docker", ["buildx", "build", ...args, "-f", "-", contextDir], { + env: { ...process.env, ...this.spec.environmentVariables }, + input: this.dockerfileBuilder.toString(), + stdio: [/* stdin= */ "pipe", /* stdout= */ "inherit", /* stderr= */ "inherit"], + }); + } + + private buildStage(stage: string, contextDir: string, tag?: string): void { + console.log(`Building stage: ${stage}`); + const args = ["--target", stage]; + if (tag) { + args.push("--tag", tag); + } + const ret = this.execDockerBuild(args, contextDir); + if (ret.error || ret.status !== 0) { + throw new Error(`Failed to execute stage ${stage}: error=${ret.error} status=${ret.status}`); + } + } + + private exportBundle(stage: string, contextDir: string): AppBundle { + const exportStage = `${stage}-export`; + this.dockerfileBuilder + .tempFrom("scratch", exportStage) + .copyFrom(BUNDLE_PATH, "/bundle.json", stage); + const ret = this.execDockerBuild( + ["--target", exportStage, "--output", ".firebase/.output"], + contextDir, + ); + if (ret.error || ret.status !== 0) { + throw new Error(`Failed to export bundle ${stage}: error=${ret.error} status=${ret.status}`); + } + return JSON.parse(fs.readFileSync("./.firebase/.output/bundle.json", "utf8")) as AppBundle; + } + + install(): void { + if (this.spec.installCommand) { + this.dockerfileBuilder + .fromLastStage(DOCKER_STAGE_INSTALL) + .workdir("/home/firebase/app") + .envs(this.spec.environmentVariables || {}) + .copyForFirebase("package.json", "."); + if (this.spec.packageManagerInstallCommand) { + this.dockerfileBuilder.run(this.spec.packageManagerInstallCommand); + } + this.dockerfileBuilder.run(this.spec.installCommand); + this.buildStage(DOCKER_STAGE_INSTALL, "."); + } + } + + build(): void { + if (this.spec.detectedCommands?.build) { + this.dockerfileBuilder + .fromLastStage(DOCKER_STAGE_BUILD) + .copyForFirebase(".", ".") + .run(this.spec.detectedCommands.build.cmd); + this.buildStage(DOCKER_STAGE_BUILD, "."); + } + } + + export(bundle: AppBundle): void { + const startCmd = bundle.server?.start.cmd; + if (startCmd) { + const exportStage = "exporter"; + this.dockerfileBuilder + .from(this.spec.baseImage, exportStage) + .workdir("/home/firebase/app") + .copyForFirebase("/home/firebase/app", ".", DOCKER_STAGE_BUILD) + .cmd(startCmd); + const imageName = `us-docker.pkg.dev/${process.env.PROJECT_ID}/test/demo-nodappe`; + this.buildStage(exportStage, ".", imageName); + const ret = this.execDockerPush([imageName]); + if (ret.error || ret.status !== 0) { + throw new Error( + `Failed to push image ${imageName}: error=${ret.error} status=${ret.status}`, + ); + } + } + } + + execHook(bundle: AppBundle, hook: Hook): AppBundle { + // Prepare hook execution by writing the node script locally + const hookScript = `hook-${Date.now()}.js`; + const hookScriptSrc = genHookScript(bundle, hook); + + if (!fs.existsSync(ADAPTER_SCRIPTS_PATH)) { + fs.mkdirSync(ADAPTER_SCRIPTS_PATH, { recursive: true }); + } + fs.writeFileSync(path.join(ADAPTER_SCRIPTS_PATH, hookScript), hookScriptSrc); + + // Execute the hook inside the docker sandbox + const hookStage = path.basename(hookScript, ".js"); + this.dockerfileBuilder + .fromLastStage(hookStage) + .run( + `NODE_PATH=./node_modules node /framework/adapters/${hookScript}`, + `source=${ADAPTER_SCRIPTS_PATH},target=/framework/adapters`, + ); + this.buildStage(hookStage, "."); + return this.exportBundle(hookStage, "."); + } +} diff --git a/src/frameworks/compose/driver/hooks.spec.ts b/src/frameworks/compose/driver/hooks.spec.ts new file mode 100644 index 00000000000..fbb44189e4e --- /dev/null +++ b/src/frameworks/compose/driver/hooks.spec.ts @@ -0,0 +1,42 @@ +import { expect } from "chai"; +import { genHookScript } from "./hooks"; +import { AppBundle } from "../interfaces"; + +describe("genHookScript", () => { + const BUNDLE: AppBundle = { + version: "v1alpha", + }; + + it("generates executable script from anonymous functions", () => { + const hookFn = (b: AppBundle): AppBundle => { + return b; + }; + const expectedSnippet = `const bundle = ((b) => { + return b; + })({"version":"v1alpha"});`; + expect(genHookScript(BUNDLE, hookFn)).to.include(expectedSnippet); + }); + + it("generates executable script from a named function", () => { + function hookFn(b: AppBundle): AppBundle { + return b; + } + const expectedSnippet = `const bundle = (function hookFn(b) { + return b; + })({"version":"v1alpha"});`; + expect(genHookScript(BUNDLE, hookFn)).to.include(expectedSnippet); + }); + + it("generates executable script from an object method", () => { + const a = { + hookFn(b: AppBundle) { + return b; + }, + }; + const expectedSnippet = `const bundle = (function hookFn(b) { + return b; + })({"version":"v1alpha"});`; + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(genHookScript(BUNDLE, a.hookFn)).to.include(expectedSnippet); + }); +}); diff --git a/src/frameworks/compose/driver/hooks.ts b/src/frameworks/compose/driver/hooks.ts new file mode 100644 index 00000000000..e4fa8aa7182 --- /dev/null +++ b/src/frameworks/compose/driver/hooks.ts @@ -0,0 +1,35 @@ +import { AppBundle, Hook } from "../interfaces"; + +export const BUNDLE_PATH = "/home/firebase/app/.firebase/.output/bundle.json" as const; + +/** + * Generate a script that wraps the given hook to output the resulting AppBundle + * to a well-known path. + */ +export function genHookScript(bundle: AppBundle, hook: Hook): string { + let hookSrc = hook.toString().trimLeft(); + // Hook must be IIFE-able. All hook functions are IFFE-able without modification + // except for function defined inside an object in the following form: + // + // { + // afterInstall(b) { + // ... + // . } + // } + // + // We detect and transform function defined in this form by prefixing "functions " + if (!hookSrc.startsWith("(") && !hookSrc.startsWith("function ")) { + hookSrc = `function ${hookSrc}`; + } + return ` +const fs = require("node:fs"); +const path = require("node:path"); + +const bundleDir = path.dirname("${BUNDLE_PATH}"); +if (!fs.existsSync(bundleDir)) { + fs.mkdirSync(bundleDir, { recursive: true }); +} +const bundle = (${hookSrc})(${JSON.stringify(bundle)}); +fs.writeFileSync("${BUNDLE_PATH}", JSON.stringify(bundle)); +`; +} diff --git a/src/frameworks/compose/driver/index.ts b/src/frameworks/compose/driver/index.ts new file mode 100644 index 00000000000..1779fdb5c3e --- /dev/null +++ b/src/frameworks/compose/driver/index.ts @@ -0,0 +1,20 @@ +import { Driver } from "../interfaces"; +import { LocalDriver } from "./local"; +import { DockerDriver } from "./docker"; +import { RuntimeSpec } from "../discover/types"; + +export const SUPPORTED_MODES = ["local", "docker"] as const; +export type Mode = (typeof SUPPORTED_MODES)[number]; + +/** + * Returns the driver that provides the execution context for the composer. + */ +export function getDriver(mode: Mode, app: RuntimeSpec): Driver { + if (mode === "local") { + return new LocalDriver(app); + } else if (mode === "docker") { + return new DockerDriver(app); + } + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unsupported mode ${mode}`); +} diff --git a/src/frameworks/compose/driver/local.ts b/src/frameworks/compose/driver/local.ts new file mode 100644 index 00000000000..f20af3583bc --- /dev/null +++ b/src/frameworks/compose/driver/local.ts @@ -0,0 +1,55 @@ +import * as fs from "node:fs"; +import * as spawn from "cross-spawn"; + +import { AppBundle, Hook, Driver } from "../interfaces"; +import { BUNDLE_PATH, genHookScript } from "./hooks"; +import { RuntimeSpec } from "../discover/types"; + +export class LocalDriver implements Driver { + constructor(readonly spec: RuntimeSpec) {} + + private execCmd(cmd: string, args: string[]) { + const ret = spawn.sync(cmd, args, { + env: { ...process.env, ...this.spec.environmentVariables }, + stdio: [/* stdin= */ "pipe", /* stdout= */ "inherit", /* stderr= */ "inherit"], + }); + if (ret.error) { + throw ret.error; + } + } + + install(): void { + if (this.spec.installCommand) { + if (this.spec.packageManagerInstallCommand) { + const [cmd, ...args] = this.spec.packageManagerInstallCommand.split(" "); + this.execCmd(cmd, args); + } + const [cmd, ...args] = this.spec.installCommand.split(" "); + this.execCmd(cmd, args); + } + } + + build(): void { + if (this.spec.detectedCommands?.build) { + const [cmd, ...args] = this.spec.detectedCommands.build.cmd.split(" "); + this.execCmd(cmd, args); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + export(bundle: AppBundle): void { + // no-op + } + + execHook(bundle: AppBundle, hook: Hook): AppBundle { + const script = genHookScript(bundle, hook); + this.execCmd("node", ["-e", script]); + if (!fs.existsSync(BUNDLE_PATH)) { + console.warn(`Expected hook to generate app bundle at ${BUNDLE_PATH} but got nothing.`); + console.warn("Returning original bundle."); + return bundle; + } + const newBundle = JSON.parse(fs.readFileSync(BUNDLE_PATH, "utf8")); + return newBundle as AppBundle; + } +} diff --git a/src/frameworks/compose/index.ts b/src/frameworks/compose/index.ts new file mode 100644 index 00000000000..945d8e1b78c --- /dev/null +++ b/src/frameworks/compose/index.ts @@ -0,0 +1,43 @@ +import { AppBundle } from "./interfaces"; +import { getDriver, Mode } from "./driver"; +import { discover } from "./discover"; +import { FrameworkSpec, FileSystem } from "./discover/types"; + +/** + * Run composer in the specified execution context. + */ +export async function compose( + mode: Mode, + fs: FileSystem, + allFrameworkSpecs: FrameworkSpec[], +): Promise { + let bundle: AppBundle = { version: "v1alpha" }; + const spec = await discover(fs, allFrameworkSpecs); + const driver = getDriver(mode, spec); + + if (spec.detectedCommands?.run) { + bundle.server = { + start: { + cmd: spec.detectedCommands.run.cmd.split(" "), + }, + }; + } + + driver.install(); + if (spec.frameworkHooks?.afterInstall) { + bundle = driver.execHook(bundle, spec.frameworkHooks.afterInstall); + } + + driver.build(); + if (spec.frameworkHooks?.afterBuild) { + bundle = driver.execHook(bundle, spec.frameworkHooks?.afterBuild); + } + + if (bundle.server) { + // Export container + driver.export(bundle); + } + + // TODO: Update stack config + return bundle; +} diff --git a/src/frameworks/compose/interfaces.ts b/src/frameworks/compose/interfaces.ts new file mode 100644 index 00000000000..068b28a178b --- /dev/null +++ b/src/frameworks/compose/interfaces.ts @@ -0,0 +1,49 @@ +import { RuntimeSpec } from "./discover/types"; + +export interface AppBundle { + version: "v1alpha"; + server?: ServerConfig; +} + +interface ServerConfig { + start: StartConfig; + concurrency?: number; + cpu?: number; + memory?: "256MiB" | "512MiB" | "1GiB" | "2GiB" | "4GiB" | "8GiB" | "16GiB" | string; + timeoutSeconds?: number; + minInstances?: number; + maxInstances?: number; +} + +interface StartConfig { + // Path to local source directory. Defaults to .bundle/server. + dir?: string; + // Command to start the server (e.g. ["npm", "run", "start"]). + cmd: string[]; + // Runtime required to command execution. + runtime?: "nodejs18" | string; +} + +export type Hook = (b: AppBundle) => AppBundle; + +export class Driver { + constructor(readonly spec: RuntimeSpec) {} + + install(): void { + throw new Error("install() not implemented"); + } + + build(): void { + throw new Error("build() not implemented"); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + export(bundle: AppBundle): void { + throw new Error("export() not implemented"); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + execHook(bundle: AppBundle, hook: Hook): AppBundle { + throw new Error("execHook() not implemented"); + } +} diff --git a/src/frameworks/constants.ts b/src/frameworks/constants.ts new file mode 100644 index 00000000000..6f891227f2a --- /dev/null +++ b/src/frameworks/constants.ts @@ -0,0 +1,82 @@ +import { SupportLevel } from "./interfaces"; +import * as clc from "colorette"; +import * as experiments from "../experiments"; + +export const NPM_COMMAND_TIMEOUT_MILLIES = 10_000; + +export const SupportLevelWarnings = { + [SupportLevel.Experimental]: (framework: string) => `Thank you for trying our ${clc.italic( + "experimental", + )} support for ${framework} on Firebase Hosting. + ${clc.red(`While this integration is maintained by Googlers it is not a supported Firebase product. + Issues filed on GitHub will be addressed on a best-effort basis by maintainers and other community members.`)}`, + [SupportLevel.Preview]: (framework: string) => `Thank you for trying our ${clc.italic( + "early preview", + )} of ${framework} support on Firebase Hosting. + ${clc.red( + "During the preview, support is best-effort and breaking changes can be expected. Proceed with caution.", + )}`, +}; + +export const DEFAULT_DOCS_URL = + "https://firebase.google.com/docs/hosting/frameworks/frameworks-overview"; +export const FILE_BUG_URL = + "https://github.com/firebase/firebase-tools/issues/new?template=bug_report.md"; +export const FEATURE_REQUEST_URL = + "https://github.com/firebase/firebase-tools/issues/new?template=feature_request.md"; +export const MAILING_LIST_URL = "https://goo.gle/41enW5X"; + +const DEFAULT_FIREBASE_FRAMEWORKS_VERSION = "^0.11.0"; +export const FIREBASE_FRAMEWORKS_VERSION = + (experiments.isEnabled("internaltesting") && process.env.FIREBASE_FRAMEWORKS_VERSION) || + DEFAULT_FIREBASE_FRAMEWORKS_VERSION; +export const FIREBASE_FUNCTIONS_VERSION = "^4.5.0"; +export const FIREBASE_ADMIN_VERSION = "^11.11.1"; +export const SHARP_VERSION = "^0.32.1"; +export const NODE_VERSION = parseInt(process.versions.node, 10); +export const VALID_ENGINES = { node: [16, 18, 20] }; + +export const VALID_LOCALE_FORMATS = [/^ALL_[a-z]+$/, /^[a-z]+_ALL$/, /^[a-z]+(_[a-z]+)?$/]; + +export const DEFAULT_REGION = "us-central1"; +export const ALLOWED_SSR_REGIONS = [ + { name: "us-central1 (Iowa)", value: "us-central1", recommended: true }, + { name: "us-east1 (South Carolina)", value: "us-east1", recommended: true }, + { name: "us-east4 (Northern Virginia)", value: "us-east4" }, + { name: "us-west1 (Oregon)", value: "us-west1", recommended: true }, + { name: "us-west2 (Los Angeles)", value: "us-west2" }, + { name: "us-west3 (Salt Lake City)", value: "us-west3" }, + { name: "us-west4 (Las Vegas)", value: "us-west4" }, + { name: "asia-east1 (Taiwan)", value: "asia-east1", recommended: true }, + { name: "asia-east2 (Hong Kong)", value: "asia-east2" }, + { name: "asia-northeast1 (Tokyo)", value: "asia-northeast1" }, + { name: "asia-northeast2 (Osaka)", value: "asia-northeast2" }, + { name: "asia-northeast3 (Seoul)", value: "asia-northeast3" }, + { name: "asia-south1 (Mumbai)", value: "asia-south1" }, + { name: "asia-south2 (Delhi)", value: "asia-south2" }, + { name: "asia-southeast1 (Singapore)", value: "asia-southeast1" }, + { name: "asia-southeast2 (Jakarta)", value: "asia-southeast2" }, + { name: "australia-southeast1 (Sydney)", value: "australia-southeast1" }, + { name: "australia-southeast2 (Melbourne)", value: "australia-southeast2" }, + { name: "europe-central2 (Warsaw)", value: "europe-central2" }, + { name: "europe-north1 (Finland)", value: "europe-north1" }, + { name: "europe-west1 (Belgium)", value: "europe-west1", recommended: true }, + { name: "europe-west2 (London)", value: "europe-west2" }, + { name: "europe-west3 (Frankfurt)", value: "europe-west3" }, + { name: "europe-west4 (Netherlands)", value: "europe-west4" }, + { name: "europe-west6 (Zurich)", value: "europe-west6" }, + { name: "northamerica-northeast1 (Montreal)", value: "northamerica-northeast1" }, + { name: "northamerica-northeast2 (Toronto)", value: "northamerica-northeast2" }, + { name: "southamerica-east1 (São Paulo)", value: "southamerica-east1" }, + { name: "southamerica-west1 (Santiago)", value: "southamerica-west1" }, +]; + +export const I18N_ROOT = "/"; + +export function GET_DEFAULT_BUILD_TARGETS() { + return Promise.resolve(["production", "development"]); +} + +export function DEFAULT_SHOULD_USE_DEV_MODE_HANDLE(target: string) { + return Promise.resolve(target === "development"); +} diff --git a/src/frameworks/docs/README.md b/src/frameworks/docs/README.md new file mode 100644 index 00000000000..a5239b1ae44 --- /dev/null +++ b/src/frameworks/docs/README.md @@ -0,0 +1,90 @@ +# Web frameworks docs on firebase.google.com + +This directory contains the documentation +for experimental framework support as well as source that is used for +preview-level support on https://firebase.google.com/docs/. + +We welcome your contributions! See [`CONTRIBUTING.md`](../CONTRIBUTING.md) for general +guidelines. This README has some information on how our documentation is organized and +some non-standard extensions we use. + +## Docs for preview-level vs experimental framework support + +If you are developing **experimental** support for a web framework, you should +follow the outline and example presented in `astro.md`. Details for your framework are +likely to be different, but the overall outline should probably be similar. + +If your framwork is entering **preview** status, its documentation will be displayed +on firebase.google.com, which may entail some extra work regarding page fragments +(see next section). Preview docs should follow the outline and example presented in +`angular.md`. Make sure to add all key details specific to your particular framework. + +Firebase follows the [Google developer documentation style guide](https://developers.google.com/style), +which you should read before writing substantial contributions. + + +## Standalone files vs. page fragments + +There are two kinds of source file for our docs: + +- **Standalone files** map one-to-one to a single page on firebase.google.com. + These files are mostly-standard Markdown with filenames that correspond with + the URL at which they're eventually published. + + Standalone pages must have filenames that don't begin with an + underscore (`_`). For example, `angular.md` in this folder is + a standalone file. + +- **Page fragments** are included in other pages. We use page fragments either + to include common text in multiple pages or to help organize large pages. + Like standalone files, page fragments are also mostly-standard Markdown, but + their filenames often don't correspond with the URL at which they're + eventually published. + + Page fragments almost always have filenames that begin with an underscore + (`_`). For example, `_before-you-begin.md` is a file of standard steps that + should be included in all frameworks integration guides in this folder. + +## Non-standard Markdown + +### File includes + +> Probably not useful to you as a contributor, but documented FYI. +> We use double angle brackets to include content from another file: + +``` +<> +``` + +Note that the path is based on our internal directory structure, and not the +layout on GitHub. Also note that we sometimes use this to include non-Web frameworks +related content that's not on GitHub. + +### Page metadata + +> Probably not useful to you as a contributor, but documented FYI. +> Every standalone page begins with the following header: + +``` +Project: /docs/_project.yaml +Book: /docs/_book.yaml +``` + +These are non-standard metadata declarations used by our internal publishing +system. There's nothing you can really do with this, but it has to be on every +standalone page. + +Footer +© 2023 GitHub, Inc. +Footer navigation +Terms +Privacy +Security +Status +Docs +Contact GitHub +Pricing +API +Training +Blog +About diff --git a/src/frameworks/docs/_includes/_before-you-begin.md b/src/frameworks/docs/_includes/_before-you-begin.md new file mode 100644 index 00000000000..2811dbad248 --- /dev/null +++ b/src/frameworks/docs/_includes/_before-you-begin.md @@ -0,0 +1,10 @@ +## Before you begin + +Before you get started deploying your app to Firebase, +review the following requirements and options: + +- {{firebase_cli}} version 12.1.0 or later. Make sure to + [install the {{cli}}](/docs/cli#install_the_firebase_cli) + using your preferred method. +- Optional: Billing enabled on your Firebase project + (required if you plan to use SSR) diff --git a/src/frameworks/docs/_includes/_initialize-firebase.md b/src/frameworks/docs/_includes/_initialize-firebase.md new file mode 100644 index 00000000000..d143ccd98cc --- /dev/null +++ b/src/frameworks/docs/_includes/_initialize-firebase.md @@ -0,0 +1,12 @@ +## Initialize Firebase + +To get started, initialize Firebase for your framework project. +Use the {{firebase_cli}} for a new project, or modify `firebase.json` for an +existing project. + +### Initialize a new project + +1. In the {{firebase_cli}}, enable the web frameworks preview: +
    firebase experiments:enable webframeworks
    +1. Run the initialization command from the {{cli}} and then follow the prompts: +
    firebase init hosting
    diff --git a/src/frameworks/docs/_includes/_preview-disclaimer.md b/src/frameworks/docs/_includes/_preview-disclaimer.md new file mode 100644 index 00000000000..64eccb2e75a --- /dev/null +++ b/src/frameworks/docs/_includes/_preview-disclaimer.md @@ -0,0 +1,4 @@ +Note: Framework-aware {{hosting}} is an early public preview. This means +that the functionality might change in backward-incompatible ways. A preview +release is not subject to any SLA or deprecation policy and may receive limited +or no support. diff --git a/src/frameworks/docs/angular.md b/src/frameworks/docs/angular.md new file mode 100644 index 00000000000..a45b098c33e --- /dev/null +++ b/src/frameworks/docs/angular.md @@ -0,0 +1,143 @@ +Project: /docs/hosting/_project.yaml +Book: /docs/_book.yaml +page_type: guide + +{% include "_shared/apis/console/_local_variables.html" %} +{% include "_local_variables.html" %} +{% include "docs/hosting/_local_variables.html" %} + + + +# Integrate Angular + +With the Firebase framework-aware {{cli}}, you can deploy your Angular application +to Firebase and serve dynamic content to your users. + +<<_includes/_preview-disclaimer.md>> + +<<_includes/_before-you-begin.md>> + +- Optional: AngularFire + +<<_includes/_initialize-firebase.md>> + +1. Answer yes to "Do you want to use a web framework? (experimental)" +1. Choose your hosting source directory; this could be an existing Angular app. +1. If prompted, choose Angular. + +### Initialize an existing project + +Change your hosting config in `firebase.json` to have a `source` option, rather +than a `public` option. For example: + +```json +{ + "hosting": { + "source": "./path-to-your-angular-workspace" + } +} +``` + +## Serve static content + +After initializing Firebase, you can serve static content with the standard +deployment command: + +```shell +firebase deploy +``` + +## Pre-render dynamic content + +To prerender dynamic content in Angular, you need to set up Angular SSR. + +```shell +ng add @angular/ssr +``` + +See the [Angular Prerendering (SSG) guide](https://angular.dev/guide/prerendering) +for more information. + +#### Deploy + +When you deploy with `firebase deploy`, Firebase builds your browser bundle, +your server bundle, and prerenders the application. These elements are deployed +to {{hosting}} and {{cloud_functions_full}}. + +#### Custom deploy + +The {{firebase_cli}} assumes that you have a single application defined in your +`angular.json` with a production build configuration. + +If need to tailor the {{cli}}'s assumptions, you can either use the +`FIREBASE_FRAMEWORKS_BUILD_TARGET` environment variable or add +[AngularFire](https://github.com/angular/angularfire#readme) and modify your +`angular.json`: + +```json +{ + "deploy": { + "builder": "@angular/fire:deploy", + "options": { + "version": 2, + "buildTarget": "OVERRIDE_YOUR_BUILD_TARGET" + } + } +} +``` + +### Optional: integrate with the Firebase JS SDK + +When including Firebase JS SDK methods in both server and client bundles, guard +against runtime errors by checking `isSupported()` before using the product. +Not all products are [supported in all environments](/docs/web/environments-js-sdk#other_environments). + +Tip: consider using [AngularFire](https://github.com/angular/angularfire#readme), +which does this for you automatically. + +### Optional: integrate with the Firebase Admin SDK + +Admin bundles will fail if they are included in your browser build, so consider +providing them in your server module and injecting as an optional dependency: + +```typescript +// your-component.ts +import type { app } from 'firebase-admin'; +import { FIREBASE_ADMIN } from '../app.module'; + +@Component({...}) +export class YourComponent { + + constructor(@Optional() @Inject(FIREBASE_ADMIN) admin: app.App) { + ... + } +} + +// app.server.module.ts +import * as admin from 'firebase-admin'; +import { FIREBASE_ADMIN } from './app.module'; + +@NgModule({ + … + providers: [ + … + { provide: FIREBASE_ADMIN, useFactory: () => admin.apps[0] || admin.initializeApp() } + ], +}) +export class AppServerModule {} + +// app.module.ts +import type { app } from 'firebase-admin'; + +export const FIREBASE_ADMIN = new InjectionToken('firebase-admin'); +``` + +## Serve fully dynamic content with SSR + +### Optional: integrate with Firebase Authentication + +The web framework-aware Firebase deployment tooling automatically keeps client +and server state in sync using cookies. The Express `res.locals` object will +optionally contain an authenticated Firebase App instance (`firebaseApp`) and +the currently signed in user (`currentUser`). This can be injected into your +module via the REQUEST token (exported from @nguniversal/express-engine/tokens). diff --git a/src/frameworks/docs/astro.md b/src/frameworks/docs/astro.md new file mode 100644 index 00000000000..4bf342dcb09 --- /dev/null +++ b/src/frameworks/docs/astro.md @@ -0,0 +1,77 @@ +# Integrate Astro + +Using the Firebase CLI, you can deploy your Astro Web apps to Firebase and +serve them with Firebase Hosting. The CLI respects your Astro settings and +translates them to Firebase settings with zero or minimal extra configuration on +your part. If your app includes dynamic server-side logic, the CLI deploys that +logic to Cloud Functions for Firebase. + +Note: Framework-aware Hosting is an early public preview. This means +that the functionality might change in backward-incompatible ways. A preview +release is not subject to any SLA or deprecation policy and may receive limited +or no support. + +## Before you begin + +Before you get started deploying your app to Firebase, +review the following requirements and options: + +- Firebase CLI version 12.1.0 or later. Make sure to + [install the CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli) + using your preferred method. +- Optional: Billing enabled on your Firebase project + (required if you plan to use SSR) +- An existing Astro project. You can create one with `npm init astro@latest`. + + +## Initialize Firebase + +To get started, initialize Firebase for your framework project. +Use the Firebase CLI for a new project, or modify `firebase.json` for an +existing project. + +### Initialize a new project + +1. In the Firebase CLI, enable the web frameworks preview: +
    firebase experiments:enable webframeworks
    +1. Run the initialization command from the CLI and then follow the prompts: +
    firebase init hosting
    +1. Answer yes to "Do you want to use a web framework? (experimental)" +1. Choose your hosting source directory. If there is an existing Astro codebase, + the CLI detects it and the process completes. + +## Serve static content + +After initializing Firebase, you can serve static content with the standard +deployment command: + +```shell +firebase deploy +``` + +You can [view your deployed app](/docs/hosting/test-preview-deploy#view-changes) +on its live site. + +## Pre-render dynamic content + +Astro will prerender all pages to static files and will work on Firebase Hosting without any configuration changes. + +If you need a small set of pages to SSR, configure `output: 'hybrid'` as +shown in +[converting a static site to hybrid rendering](https://docs.astro.build/en/guides/server-side-rendering/#converting-a-static-site-to-hybrid-rendering`). + +With these settings prerendering is still the default, but you can opt in to SSR by +adding `const prerender = false` at the top of any Astro page. Similarly, +in `output: 'server'` where +server rendering is the default you can opt in to prerendering by adding +`const prerender = true`. + +## Serve fully dynamic content (SSR) + +Deploying an Astro application with SSR on Firebase Hosting requires the +@astrojs/node adapter in middleware mode. See the detailed instructions in the +Astro docs for setting up the +[node adapter](https://docs.astro.build/en/guides/integrations-guide/node/) +and for [SSR](https://docs.astro.build/en/guides/server-side-rendering/). + +As noted in the Astro guidance, SSR also requires setting the `output` property to either `server` or `hybrid` in `astro.config.mjs`. diff --git a/src/frameworks/docs/express.md b/src/frameworks/docs/express.md new file mode 100644 index 00000000000..53c38d6e704 --- /dev/null +++ b/src/frameworks/docs/express.md @@ -0,0 +1,181 @@ +Project: /docs/hosting/_project.yaml +Book: /docs/_book.yaml +page_type: guide + +{% include "_shared/apis/console/_local_variables.html" %} +{% include "_local_variables.html" %} +{% include "docs/hosting/_local_variables.html" %} + + + +# Integrate other frameworks with Express.js + +With some additional configuration, you can build on the basic +framework-aware {{cli}} functionality +to extend integration support to frameworks other than Angular and Next.js. + +<<_includes/_preview-disclaimer.md>> + +<<_includes/_before-you-begin.md>> + +<<_includes/_initialize-firebase.md>> + +1. Answer yes to "Do you want to use a web framework? (experimental)" +1. Choose your hosting source directory; this could be an existing web app. +1. If prompted, choose Express.js / custom + +### Initialize an existing project + +Change your hosting config in `firebase.json` to have a `source` option, rather +than a `public` option. For example: + +```json +{ + "hosting": { + "source": "./path-to-your-express-directory" + } +} +``` + +## Serve static content + +Before deploying static content, you'll need to configure your application. + +### Configure + +In order to know how to deploy your application, the {{firebase_cli}} needs to be +able to both build your app and know where your tooling places the assets +destined for {{hosting}}. This is accomplished with the npm build script and CJS +directories directive in `package.json`. + +Given the following package.json: + +```json +{ + "name": "express-app", + "version": "0.0.0", + "scripts": { + "build": "spack", + "static": "cp static/* dist", + "prerender": "ts-node prerender.ts" + }, + … +} +``` + +The {{firebase_cli}} only calls your build script, so you’ll need to ensure that +your build script is exhaustive. + +Tip: you can add additional steps using` &&`. If you have a lot of steps, +consider a shell script or tooling like [npm-run-all](https://www.npmjs.com/package/npm-run-all) +or [wireit](https://www.npmjs.com/package/wireit). + +```json +{ + "name": "express-app", + "version": "0.0.0", + "scripts": { + "build": "spack && npm run static && npm run prerender", + "static": "cp static/* dist", + "prerender": "ts-node prerender.ts" + }, + … +} +``` + +If your framework doesn’t support pre-rendering out of the box, consider using a +tool like [Rendertron](https://github.com/GoogleChrome/rendertron). Rendertron +will allow you to make headless Chrome requests against a local instance of your +app, so you can save the resulting HTML to be served on {{hosting}}. + +Finally, different frameworks and build tools store their artifacts in different +places. Use `directories.serve` to tell the {{cli}} where your build script is +outputting the resulting artifacts: + +```json +{ + "name": "express-app", + "version": "0.0.0", + "scripts": { + "build": "spack && npm run static && npm run prerender", + "static": "cp static/* dist", + "prerender": "ts-node prerender.ts" + }, + "directories": { + "serve": "dist" + }, + … +} +``` + +### Deploy + +After configuring your app, you can serve static content with the standard +deployment command: + +```shell +firebase deploy +``` + +## Serve Dynamic Content + +To serve your Express app on {{cloud_functions_full}}, ensure that your Express app (or +express-style URL handler) is exported in such a way that Firebase can find it +after your library has been npm packed. + +To accomplish this, ensure that your `files` directive includes everything +needed for the server, and that your main entry point is set up correctly in +`package.json`: + +```json +{ + "name": "express-app", + "version": "0.0.0", + "scripts": { + "build": "spack && npm run static && npm run prerender", + "static": "cp static/* dist", + "prerender": "ts-node tools/prerender.ts" + }, + "directories": { + "serve": "dist" + }, + "files": ["dist", "server.js"], + "main": "server.js", + ... +} +``` + +Export your express app from a function named `app`: + +```js +// server.js +export function app() { + const server = express(); + … + return server; +} +``` + +Or if you’d rather export an express-style URL handler, name it `handle`: + +```js +export function handle(req, res) { + res.send(‘hello world’); +} +``` + +### Deploy + +```shell +firebase deploy +``` + +This deploys your static content to {{firebase_hosting}} and allows Firebase to +fall back to your Express app hosted on {{cloud_functions_full}}. + +## Optional: integrate with Firebase Authentication + +The web framework-aware Firebase deploy tooling will automatically keep client +and server state in sync using cookies. To access the authentication context, +the Express `res.locals` object optionally contains an authenticated Firebase +App instance (`firebaseApp`) and the currently signed in User (`currentUser`). diff --git a/src/frameworks/docs/flutter.md b/src/frameworks/docs/flutter.md new file mode 100644 index 00000000000..c54274b060d --- /dev/null +++ b/src/frameworks/docs/flutter.md @@ -0,0 +1,46 @@ +Project: /docs/hosting/_project.yaml +Book: /docs/_book.yaml +page_type: guide + +{% include "_shared/apis/console/_local_variables.html" %} +{% include "_local_variables.html" %} +{% include "docs/hosting/_local_variables.html" %} + + + +# Integrate Flutter Web + +With the Firebase framework-aware {{cli}}, you can deploy your Flutter application +to Firebase. + +<<_includes/_preview-disclaimer.md>> + +<<_includes/_before-you-begin.md>> + +<<_includes/_initialize-firebase.md>> + +1. Answer yes to "Do you want to use a web framework? (experimental)" +1. Choose your hosting source directory; this could be an existing Flutter app. +1. If prompted, choose Flutter Web. + +### Initialize an existing project + +Change your hosting config in `firebase.json` to have a `source` option, rather +than a `public` option. For example: + +```json +{ + "hosting": { + "source": "./path-to-your-flutter-app" + } +} +``` + +## Serve static content + +After initializing Firebase, you can serve static content with the standard +deployment command: + +```shell +firebase deploy +``` diff --git a/src/frameworks/docs/frameworks-overview.md b/src/frameworks/docs/frameworks-overview.md new file mode 100644 index 00000000000..a8b1d3e6ff4 --- /dev/null +++ b/src/frameworks/docs/frameworks-overview.md @@ -0,0 +1,60 @@ +Project: /docs/hosting/_project.yaml +Book: /docs/_book.yaml +page_type: guide + +{% include "_shared/apis/console/_local_variables.html" %} +{% include "_local_variables.html" %} +{% include "docs/hosting/_local_variables.html" %} + + + +# Integrate web frameworks with {{hosting}} + +{{firebase_hosting}} integrates with popular modern web frameworks including Angular +and Next.js. Using {{firebase_hosting}} and {{cloud_functions_full}} with these +frameworks, you can develop apps and microservices in your preferred framework +environment, and then deploy them in a managed, secure server environment. + +Note: Experimental support for Flask and Django is under development, and will be +available soon. To stay up to date on the latest releases, sign up as a +trusted tester at [https://goo.gle/41enW5X](//goo.gle/41enW5X). + +Support during this early preview includes the following functionality: + +* Deploy Web apps comprised of static web content +* Deploy Web apps that use pre-rendering / Static Site Generation (SSG) +* Deploy Web apps that use server-side Rendering (SSR)—full server rendering on demand + +Firebase provides this functionality through the {{firebase_cli}}. When initializing +{{hosting}} on the command line, you provide information about your new or existing +Web project, and the {{cli}} sets up the right resources for your chosen Web +framework. + +<<_includes/_preview-disclaimer.md>> + +<<_includes/_before-you-begin.md>> + +## Serve locally + +You can test your integration locally by following these steps: + +1. Run `firebase emulators:start` from the terminal. This builds your app and + serves it using the {{firebase_cli}}. +2. Open your web app at the local URL returned by the {{cli}} (usually http://localhost:5000). + +## Deploy your app to {{firebase_hosting}} + +When you're ready to share your changes with the world, deploy your app to your +live site: + +1. Run `firebase deploy` from the terminal. +2. Check your website on: `SITE_ID.web.app` or `PROJECT_ID.web.app` (or your custom domain, if you set one up). + +## Next steps + +See the detailed guide for your preferred framework: + +* [Angular](/docs/hosting/frameworks/angular) +* [Next.js] (/docs/hosting/frameworks/nextjs) +* [Flutter Web] (/docs/hosting/frameworks/flutter) +* [Frameworks with Express.js](/docs/hosting/frameworks/express) diff --git a/src/frameworks/docs/index.ts b/src/frameworks/docs/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/src/frameworks/docs/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/frameworks/docs/lit.md b/src/frameworks/docs/lit.md new file mode 100644 index 00000000000..f020bede473 --- /dev/null +++ b/src/frameworks/docs/lit.md @@ -0,0 +1,3 @@ +# Integrate Lit + +Lit support is built on the Vite framework integration. See [vite.md](./vite.md) for full guidance. \ No newline at end of file diff --git a/src/frameworks/docs/nextjs.md b/src/frameworks/docs/nextjs.md new file mode 100644 index 00000000000..611bf5ae818 --- /dev/null +++ b/src/frameworks/docs/nextjs.md @@ -0,0 +1,115 @@ +Project: /docs/hosting/_project.yaml +Book: /docs/_book.yaml +page_type: guide + +{% include "_shared/apis/console/_local_variables.html" %} +{% include "_local_variables.html" %} +{% include "docs/hosting/_local_variables.html" %} + + + +# Integrate Next.js + +Using the {{firebase_cli}}, you can deploy your Next.js Web apps to Firebase and +serve them with {{firebase_hosting}}. The {{cli}} respects your Next.js settings and +translates them to Firebase settings with zero or minimal extra configuration on +your part. If your app includes dynamic server-side logic, the {{cli}} deploys that +logic to {{cloud_functions_full}}. + +<<_includes/_preview-disclaimer.md>> + +<<_includes/_before-you-begin.md>> + +- Optional: use the experimental ReactFire library to benefit from its + Firebase-friendly features + +<<_includes/_initialize-firebase.md>> + +1. Answer yes to "Do you want to use a web framework? (experimental)" +1. Choose your hosting source directory. If this an existing Next.js app, + the {{cli}} process completes, and you can proceed to the next section. +1. If prompted, choose Next.js. + +## Serve static content + +After initializing Firebase, you can serve static content with the standard +deployment command: + +```shell +firebase deploy +``` + +You can [view your deployed app](/docs/hosting/test-preview-deploy#view-changes) +on its live site. + +## Pre-render dynamic content + +The {{firebase_cli}} will detect usage of +[getStaticProps](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props) +and [getStaticPaths](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-paths). + +### Optional: integrate with the Firebase JS SDK + +When including Firebase JS SDK methods in both server and client bundles, guard +against runtime errors by checking `isSupported()` before using the product. +Not all products are +[supported in all environments](/docs/web/environments-js-sdk#other_environments). + +Tip: consider using +[ReactFire](https://github.com/FirebaseExtended/reactfire#reactfire), which does +this for you automatically. + +### Optional: integrate with the Firebase Admin SDK + +Admin SDK bundles will fail if included in your browser build; refer to them +only inside [getStaticProps](https://nextjs.org/docs/basic-features/data-fetching/get-static-props) +and [getStaticPaths](https://nextjs.org/docs/basic-features/data-fetching/get-static-paths). + +## Serve fully dynamic content (SSR) + +The {{firebase_cli}} will detect usage of +[getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props). +In such cases, the {{cli}} will deploy functions to {{cloud_functions_full}} to run dynamic +server code. You can view information about these functions, such as their domain and runtime +configuration, in the [Firebase console](https://console.firebase.google.com/project/_/functions). + + +## Configure {{hosting}} behavior with `next.config.js` + +### Image Optimization + +Using [Next.js Image Optimization](https://nextjs.org/docs/basic-features/image-optimization) +is supported, but it will trigger creation of a function +(in [{{cloud_functions_full}}](/docs/functions/)), even if you’re not using SSR. + +Note: Because of this, image optimization and {{hosting}} preview channels don’t +interoperate well together. + +### Redirects, Rewrites, and Headers + +The {{firebase_cli}} respects +[redirects](https://nextjs.org/docs/api-reference/next.config.js/redirects), +[rewrites](https://nextjs.org/docs/api-reference/next.config.js/rewrites), and +[headers](https://nextjs.org/docs/api-reference/next.config.js/headers) in +`next.config.js`, converting them to their +respective equivalent {{firebase_hosting}} configuration at deploy time. If a +Next.js redirect, rewrite, or header cannot be converted to an equivalent +{{firebase_hosting}} header, it falls back and builds a function—even if you +aren’t using image optimization or SSR. + +### Optional: integrate with Firebase Authentication + +The web framework-aware Firebase deployment tooling will automatically keep +client and server state in sync using cookies. There are some methods provided +for accessing the authentication context in SSR: + +- The Express `res.locals` object will optionally contain an authenticated + Firebase App instance (`firebaseApp`) and the currently signed-in user + (`currentUser`). This can be accessed in `getServerSideProps`. +- The authenticated Firebase App name is provided on the route query + (`__firebaseAppName`). This allows for manual integration while in context: + +```typescript +// get the authenticated Firebase App +const firebaseApp = getApp(useRouter().query.__firebaseAppName); +``` diff --git a/src/frameworks/docs/nuxt.md b/src/frameworks/docs/nuxt.md new file mode 100644 index 00000000000..068c4fd7e7c --- /dev/null +++ b/src/frameworks/docs/nuxt.md @@ -0,0 +1,61 @@ +# Integrate Nuxt + +Using the Firebase CLI, you can deploy your Nuxt apps to Firebase and +serve them with Firebase Hosting. The CLI respects your Nuxt settings and +translates them to Firebase settings with zero or minimal extra configuration on +your part. If your app includes dynamic server-side logic, the CLI deploys that +logic to Cloud Functions for Firebase. + +Note: Framework-aware Hosting is an early public preview. This means +that the functionality might change in backward-incompatible ways. A preview +release is not subject to any SLA or deprecation policy and may receive limited +or no support. + +## Before you begin + +Before you get started deploying your app to Firebase, +review the following requirements and options: + +- Firebase CLI version 12.1.0 or later. Make sure to + [install the CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli) + using your preferred method. +- Optional: Billing enabled on your Firebase project + (required if you plan to use SSR) +- An existing Nuxt (version 3+) project. You can create one with `npx nuxi@latest init `. + + +## Initialize Firebase + +To get started, initialize Firebase for your framework project. +Use the Firebase CLI for a new project, or modify `firebase.json` for an +existing project. + +### Initialize a new project + +1. In the Firebase CLI, enable the web frameworks preview: +
    firebase experiments:enable webframeworks
    +2. Run the initialization command from the CLI and then follow the prompts: +
    firebase init hosting
    + If there is an existing Nuxt codebase, the CLI detects it. + +## Deployment + +After initializing Firebase, you can deploy your Nuxt app with the standard +deployment command: + +```shell +firebase deploy +``` + +## Serve static content + +If your Nuxt app uses [`ssr: false`](https://nuxt.com/docs/api/configuration/nuxt-config#ssr), +the Firebase CLI will correctly detect and configure your build to serve fully +static content on Firebase Hosting. + +## Server-side rendering + +The Firebase CLI will detect usage of [`ssr: true`](https://nuxt.com/docs/api/configuration/nuxt-config#ssr). +In such cases, the Firebase CLI will deploy functions to Cloud Functions for Firebase to run dynamic +server code. You can view information about these functions, such as their domain and runtime +configuration, in the [Firebase console](https://console.firebase.google.com/project/_/functions). \ No newline at end of file diff --git a/src/frameworks/docs/preact.md b/src/frameworks/docs/preact.md new file mode 100644 index 00000000000..6be4f313eb0 --- /dev/null +++ b/src/frameworks/docs/preact.md @@ -0,0 +1,3 @@ +# Integrate Preact + +Preact support is built on the Vite framework integration. See [vite.md](./vite.md) for full guidance. \ No newline at end of file diff --git a/src/frameworks/docs/react.md b/src/frameworks/docs/react.md new file mode 100644 index 00000000000..4cc7807432f --- /dev/null +++ b/src/frameworks/docs/react.md @@ -0,0 +1,3 @@ +# Integrate React + +React support is built on the Vite framework integration. See [vite.md](./vite.md) for full guidance. \ No newline at end of file diff --git a/src/frameworks/docs/svelte.md b/src/frameworks/docs/svelte.md new file mode 100644 index 00000000000..9f1b75003b1 --- /dev/null +++ b/src/frameworks/docs/svelte.md @@ -0,0 +1,3 @@ +# Integrate Svelte + +Svelte support is built on the Vite framework integration. See [vite.md](./vite.md) for full guidance. \ No newline at end of file diff --git a/src/frameworks/docs/sveltekit.md b/src/frameworks/docs/sveltekit.md new file mode 100644 index 00000000000..0e2c60df24e --- /dev/null +++ b/src/frameworks/docs/sveltekit.md @@ -0,0 +1,72 @@ +# Integrate SvelteKit + +Using the Firebase CLI, you can deploy your SvelteKit apps to Firebase and +serve them with Firebase Hosting. The CLI respects your SvelteKit settings and +translates them to Firebase settings with zero or minimal extra configuration on +your part. If your app includes dynamic server-side logic, the CLI deploys that +logic to Cloud Functions for Firebase. + +Note: Framework-aware Hosting is an early public preview. This means +that the functionality might change in backward-incompatible ways. A preview +release is not subject to any SLA or deprecation policy and may receive limited +or no support. + +## Before you begin + +Before you get started deploying your app to Firebase, +review the following requirements and options: + +- Firebase CLI version 12.1.0 or later. Make sure to + [install the CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli) + using your preferred method. +- Optional: Billing enabled on your Firebase project + (required if you plan to use SSR) +- An existing SvelteKit project. You can create one with `npm init svelte@latest`. + + +## Initialize Firebase + +To get started, initialize Firebase for your framework project. +Use the Firebase CLI for a new project, or modify `firebase.json` for an +existing project. + +### Initialize a new project + +1. In the Firebase CLI, enable the web frameworks preview: +
    firebase experiments:enable webframeworks
    +1. Run the initialization command from the CLI and then follow the prompts: +
    firebase init hosting
    +1. Answer yes to "Do you want to use a web framework? (experimental)" +1. Choose your hosting source directory. + If there is an existing SvelteKit codebase, + the CLI detects it and the process completes. + +## Serve static content + +If your app uses +[`@sveltejs/adapter-static`](https://kit.svelte.dev/docs/adapter-static), +the Firebase CLI will correctly detect and configure your build to serve fully +static content on Firebase Hosting. + +## Server-side rendering + +Firebase supports both server-side rendering and a mix of prerendering and SSR. +Unless you are using `@sveltejs/adapter-static`, all pages are rendered on the +server at runtime by default, but you can opt in to prerendering for certain +routes by adding `export const prerender = true` to the relevant `+layout.js` +or `+page.js` files. +See detailed instructions for setting +[page options](https://kit.svelte.dev/docs/page-options). + +## Deployment + +If you want to deploy an entirely static site, +install and configure `@sveltejs/adapter-static`. +The static files will be deployed to Firebase Hosting, no Cloud Functions required. + +If you have a mix of static and server-rendered pages, +it is not necessary to install a special deployment adapter. +Leave the default configuration of `@sveltejs/adapter-auto`. +The necessary dynamic logic will be created and deployed to Cloud Functions. + +Run `firebase deploy` to build and deploy your SvelteKit app. \ No newline at end of file diff --git a/src/frameworks/docs/vite.md b/src/frameworks/docs/vite.md new file mode 100644 index 00000000000..181bf85efcd --- /dev/null +++ b/src/frameworks/docs/vite.md @@ -0,0 +1,51 @@ +# Integrate Vite + +Using the Firebase CLI, you can deploy your Vite-powered sites to Firebase +and serve them with Firebase Hosting. The following instructions also apply +to React, Preact, Lit, and Svelte as they are built on the Vite integration. + +Note: Framework-aware Hosting is an early public preview. This means +that the functionality might change in backward-incompatible ways. A preview +release is not subject to any SLA or deprecation policy and may receive limited +or no support. + +## Before you begin + +Before you get started deploying your app to Firebase, +review the following requirements and options: + +- Firebase CLI version 12.1.0 or later. Make sure to + [install the CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli) using your preferred + method. +- Optional: An existing Vite project. You can create one with + `npm create vite@latest` or let the Firebase CLI + initialize a new project for you. + + +## Initialize Firebase + +To get started, initialize Firebase for your framework project. +Use the Firebase CLI for a new project, or modify `firebase.json` for an +existing project. + +### Initialize a new project + +1. In the Firebase CLI, enable the web frameworks preview: +
    firebase experiments:enable webframeworks
    +1. Run the initialization command from the CLI and then follow the prompts: +
    firebase init hosting
    +1. Answer yes to "Do you want to use a web framework? (experimental)" +1. Choose your hosting source directory. If there is an existing Vite codebase, + the CLI detects it and the process completes. + +## Serve static content + +After initializing Firebase, you can serve static content with the standard +deployment command: + +```shell +firebase deploy +``` + +You can [view your deployed app](https://firebase.google.com/docs/hosting/test-preview-deploy#view-changes) +on its live site. diff --git a/src/frameworks/express/index.ts b/src/frameworks/express/index.ts new file mode 100644 index 00000000000..cc27f44ac3e --- /dev/null +++ b/src/frameworks/express/index.ts @@ -0,0 +1,110 @@ +import { execSync } from "child_process"; +import { copy, pathExists } from "fs-extra"; +import { mkdir, readFile } from "fs/promises"; +import { join } from "path"; +import { BuildResult, FrameworkType, SupportLevel } from "../interfaces"; + +// Use "true &&"" to keep typescript from compiling this file and rewriting +// the import statement into a require +const { dynamicImport } = require(true && "../../dynamicImport"); + +export const name = "Express.js"; +export const support = SupportLevel.Preview; +export const type = FrameworkType.Custom; +export const docsUrl = "https://firebase.google.com/docs/hosting/frameworks/express"; + +async function getConfig(root: string) { + const packageJsonBuffer = await readFile(join(root, "package.json")); + const packageJson = JSON.parse(packageJsonBuffer.toString()); + const serve: string | undefined = packageJson.directories?.serve; + const serveDir = serve && join(root, packageJson.directories?.serve); + return { serveDir, packageJson }; +} + +export async function discover(dir: string) { + if (!(await pathExists(join(dir, "package.json")))) return; + const { serveDir: publicDirectory } = await getConfig(dir); + if (!publicDirectory) return; + return { mayWantBackend: true, publicDirectory }; +} + +export async function build(cwd: string): Promise { + execSync(`npm run build`, { stdio: "inherit", cwd }); + const wantsBackend = !!(await getBootstrapScript(cwd)); + return { wantsBackend }; +} + +export async function ɵcodegenPublicDirectory(root: string, dest: string) { + const { serveDir } = await getConfig(root); + await copy(serveDir!, dest); +} + +async function getBootstrapScript( + root: string, + _bootstrapScript = "", + _entry?: any, +): Promise { + let entry = _entry; + let bootstrapScript = _bootstrapScript; + const allowRecursion = !entry; + if (!entry) { + const { + packageJson: { name }, + } = await getConfig(root); + try { + entry = require(root); + bootstrapScript = `const bootstrap = Promise.resolve(require('${name}'))`; + } catch (e) { + entry = await dynamicImport(root).catch(() => undefined); + bootstrapScript = `const bootstrap = import('${name}')`; + } + } + if (!entry) return undefined; + const { default: defaultExport, app, handle } = entry; + if (typeof handle === "function") { + return ( + bootstrapScript + + ";\nexports.handle = async (req, res) => (await bootstrap).handle(req, res);" + ); + } + if (typeof app === "function") { + try { + const express = app(); + if (typeof express.render === "function") { + return ( + bootstrapScript + + ";\nexports.handle = async (req, res) => (await bootstrap).app(req, res);" + ); + } + } catch (e) { + // continue, failure here is expected + } + } + if (!allowRecursion) return undefined; + if (typeof defaultExport === "object") { + bootstrapScript += ".then(({ default }) => default)"; + if (typeof defaultExport.then === "function") { + const awaitedDefaultExport = await defaultExport; + return getBootstrapScript(root, bootstrapScript, awaitedDefaultExport); + } else { + return getBootstrapScript(root, bootstrapScript, defaultExport); + } + } + return undefined; +} + +export async function ɵcodegenFunctionsDirectory(root: string, dest: string) { + const bootstrapScript = await getBootstrapScript(root); + if (!bootstrapScript) throw new Error("Cloud not find bootstrapScript"); + await mkdir(dest, { recursive: true }); + + const { packageJson } = await getConfig(root); + + const packResults = execSync(`npm pack ${root} --json`, { cwd: dest }); + const npmPackResults = JSON.parse(packResults.toString()); + const matchingPackResult = npmPackResults.find((it: any) => it.name === packageJson.name); + const { filename } = matchingPackResult; + packageJson.dependencies ||= {}; + packageJson.dependencies[packageJson.name] = `file:${filename}`; + return { bootstrapScript, packageJson }; +} diff --git a/src/frameworks/flutter/constants.ts b/src/frameworks/flutter/constants.ts new file mode 100644 index 00000000000..7381c0d9682 --- /dev/null +++ b/src/frameworks/flutter/constants.ts @@ -0,0 +1,71 @@ +// https://dart.dev/language/keywords +export const DART_RESERVED_WORDS = [ + "abstract", + "else", + "import", + "show", + "as", + "enum", + "in", + "static", + "assert", + "export", + "interface", + "super", + "async", + "extends", + "is", + "switch", + "await", + "extension", + "late", + "sync", + "base", + "external", + "library", + "this", + "break", + "factory", + "mixin", + "throw", + "case", + "false", + "new", + "true", + "catch", + "final", + "null", + "try", + "class", + "on", + "typedef", + "const", + "finally", + "operator", + "var", + "continue", + "for", + "part", + "void", + "covariant", + "function", + "required", + "when", + "default", + "get", + "rethrow", + "while", + "deferred", + "hide", + "return", + "with", + "do", + "if", + "sealed", + "yield", + "dynamic", + "implements", + "set", +]; + +export const FALLBACK_PROJECT_NAME = "hello_firebase"; diff --git a/src/frameworks/flutter/index.spec.ts b/src/frameworks/flutter/index.spec.ts new file mode 100644 index 00000000000..5214866315c --- /dev/null +++ b/src/frameworks/flutter/index.spec.ts @@ -0,0 +1,160 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { EventEmitter } from "events"; +import { Writable } from "stream"; +import * as crossSpawn from "cross-spawn"; +import * as fsExtra from "fs-extra"; +import * as fsPromises from "fs/promises"; +import { join } from "path"; + +import * as flutterUtils from "./utils"; +import { discover, build, ɵcodegenPublicDirectory, init } from "."; + +describe("Flutter", () => { + describe("discovery", () => { + const cwd = "."; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should discover", async () => { + sandbox.stub(fsExtra, "pathExists" as any).resolves(true); + sandbox + .stub(fsPromises, "readFile") + .withArgs(join(cwd, "pubspec.yaml")) + .resolves( + Buffer.from(`dependencies: + flutter: + sdk: flutter`), + ); + expect(await discover(cwd)).to.deep.equal({ + mayWantBackend: false, + }); + }); + + it("should not discover, if missing files", async () => { + sandbox.stub(fsExtra, "pathExists" as any).resolves(false); + expect(await discover(cwd)).to.be.undefined; + }); + + it("should not discovery, not flutter", async () => { + sandbox.stub(fsExtra, "pathExists" as any).resolves(true); + sandbox + .stub(fsPromises, "readFile") + .withArgs(join(cwd, "pubspec.yaml")) + .resolves( + Buffer.from(`dependencies: + foo: + bar: 1`), + ); + expect(await discover(cwd)).to.be.undefined; + }); + }); + + describe("ɵcodegenPublicDirectory", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should copy over the web dir", async () => { + const root = Math.random().toString(36).split(".")[1]; + const dist = Math.random().toString(36).split(".")[1]; + const copy = sandbox.stub(fsExtra, "copy"); + await ɵcodegenPublicDirectory(root, dist); + expect(copy.getCalls().map((it) => it.args)).to.deep.equal([ + [join(root, "build", "web"), dist], + ]); + }); + }); + + describe("build", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should build", async () => { + const process = new EventEmitter() as any; + process.stdin = new Writable(); + process.stdout = new EventEmitter(); + process.stderr = new EventEmitter(); + process.status = 0; + + sandbox.stub(flutterUtils, "assertFlutterCliExists").returns(undefined); + + const cwd = "."; + + const stub = sandbox.stub(crossSpawn, "sync").returns(process as any); + + const result = build(cwd); + + expect(await result).to.deep.equal({ + wantsBackend: false, + }); + sinon.assert.calledWith(stub, "flutter", ["build", "web"], { cwd, stdio: "inherit" }); + }); + }); + + describe("init", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should create a new project", async () => { + const process = new EventEmitter() as any; + process.stdin = new Writable(); + process.stdout = new EventEmitter(); + process.stderr = new EventEmitter(); + process.status = 0; + + sandbox.stub(flutterUtils, "assertFlutterCliExists").returns(undefined); + + const projectId = "asdflj-ao9iu4__49"; + const projectName = "asdflj_ao9iu4__49"; + const projectDir = "asfijreou5o"; + const source = "asflijrelijf"; + + const stub = sandbox.stub(crossSpawn, "sync").returns(process as any); + + const result = init({ projectId, hosting: { source } }, { projectDir }); + + expect(await result).to.eql(undefined); + sinon.assert.calledWith( + stub, + "flutter", + [ + "create", + "--template=app", + `--project-name=${projectName}`, + "--overwrite", + "--platforms=web", + source, + ], + { cwd: projectDir, stdio: "inherit" }, + ); + }); + }); +}); diff --git a/src/frameworks/flutter/index.ts b/src/frameworks/flutter/index.ts new file mode 100644 index 00000000000..283a737d659 --- /dev/null +++ b/src/frameworks/flutter/index.ts @@ -0,0 +1,62 @@ +import { sync as spawnSync } from "cross-spawn"; +import { copy, pathExists } from "fs-extra"; +import { join } from "path"; +import * as yaml from "yaml"; +import { readFile } from "fs/promises"; + +import { BuildResult, Discovery, FrameworkType, SupportLevel } from "../interfaces"; +import { FirebaseError } from "../../error"; +import { assertFlutterCliExists } from "./utils"; +import { DART_RESERVED_WORDS, FALLBACK_PROJECT_NAME } from "./constants"; + +export const name = "Flutter Web"; +export const type = FrameworkType.Framework; +export const support = SupportLevel.Experimental; + +export async function discover(dir: string): Promise { + if (!(await pathExists(join(dir, "pubspec.yaml")))) return; + if (!(await pathExists(join(dir, "web")))) return; + const pubSpecBuffer = await readFile(join(dir, "pubspec.yaml")); + const pubSpec = yaml.parse(pubSpecBuffer.toString()); + const usingFlutter = pubSpec.dependencies?.flutter; + if (!usingFlutter) return; + return { mayWantBackend: false }; +} + +export function init(setup: any, config: any) { + assertFlutterCliExists(); + // Convert the projectId into a valid pubspec name https://dart.dev/tools/pub/pubspec#name + // the projectId should be safe, save hyphens which we turn into underscores here + // if it's a reserved word just give it a fallback name + const projectName = DART_RESERVED_WORDS.includes(setup.projectId) + ? FALLBACK_PROJECT_NAME + : setup.projectId.replaceAll("-", "_"); + const result = spawnSync( + "flutter", + [ + "create", + "--template=app", + `--project-name=${projectName}`, + "--overwrite", + "--platforms=web", + setup.hosting.source, + ], + { stdio: "inherit", cwd: config.projectDir }, + ); + if (result.status !== 0) + throw new FirebaseError( + "We were not able to create your flutter app, create the application yourself https://docs.flutter.dev/get-started/test-drive?tab=terminal before trying again.", + ); + return Promise.resolve(); +} + +export function build(cwd: string): Promise { + assertFlutterCliExists(); + const build = spawnSync("flutter", ["build", "web"], { cwd, stdio: "inherit" }); + if (build.status !== 0) throw new FirebaseError("Unable to build your Flutter app"); + return Promise.resolve({ wantsBackend: false }); +} + +export async function ɵcodegenPublicDirectory(sourceDir: string, destDir: string) { + await copy(join(sourceDir, "build", "web"), destDir); +} diff --git a/src/frameworks/flutter/utils.spec.ts b/src/frameworks/flutter/utils.spec.ts new file mode 100644 index 00000000000..2635b8fa14a --- /dev/null +++ b/src/frameworks/flutter/utils.spec.ts @@ -0,0 +1,34 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { EventEmitter } from "events"; +import { Writable } from "stream"; +import * as crossSpawn from "cross-spawn"; + +import { assertFlutterCliExists } from "./utils"; + +describe("Flutter utils", () => { + describe("assertFlutterCliExists", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return void, if !status", () => { + const process = new EventEmitter() as any; + process.stdin = new Writable(); + process.stdout = new EventEmitter(); + process.stderr = new EventEmitter(); + process.status = 0; + + const stub = sandbox.stub(crossSpawn, "sync").returns(process as any); + + expect(assertFlutterCliExists()).to.be.undefined; + sinon.assert.calledWith(stub, "flutter", ["--version"], { stdio: "ignore" }); + }); + }); +}); diff --git a/src/frameworks/flutter/utils.ts b/src/frameworks/flutter/utils.ts new file mode 100644 index 00000000000..422cd6cc6e8 --- /dev/null +++ b/src/frameworks/flutter/utils.ts @@ -0,0 +1,10 @@ +import { sync as spawnSync } from "cross-spawn"; +import { FirebaseError } from "../../error"; + +export function assertFlutterCliExists() { + const process = spawnSync("flutter", ["--version"], { stdio: "ignore" }); + if (process.status !== 0) + throw new FirebaseError( + "Flutter CLI not found, follow the instructions here https://docs.flutter.dev/get-started/install before trying again.", + ); +} diff --git a/src/frameworks/frameworks.ts b/src/frameworks/frameworks.ts new file mode 100644 index 00000000000..db3ef175060 --- /dev/null +++ b/src/frameworks/frameworks.ts @@ -0,0 +1,31 @@ +import * as angular from "./angular"; +import * as astro from "./astro"; +import * as express from "./express"; +import * as lit from "./lit"; +import * as next from "./next"; +import * as nuxt from "./nuxt"; +import * as nuxt2 from "./nuxt2"; +import * as preact from "./preact"; +import * as svelte from "./svelte"; +import * as svelekit from "./sveltekit"; +import * as react from "./react"; +import * as vite from "./vite"; +import * as flutter from "./flutter"; + +import { Framework } from "./interfaces"; + +export const WebFrameworks: Record = { + angular, + astro, + express, + lit, + next, + nuxt, + nuxt2, + preact, + svelte, + svelekit, + react, + vite, + flutter, +}; diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts new file mode 100644 index 00000000000..cac8731d93e --- /dev/null +++ b/src/frameworks/index.ts @@ -0,0 +1,609 @@ +import { join, relative, basename, posix } from "path"; +import { exit } from "process"; +import { execSync } from "child_process"; +import { sync as spawnSync } from "cross-spawn"; +import { copyFile, readdir, readFile, rm, writeFile } from "fs/promises"; +import { mkdirp, pathExists, stat } from "fs-extra"; +import { glob } from "glob"; +import * as process from "node:process"; + +import { needProjectId } from "../projectUtils"; +import { hostingConfig } from "../hosting/config"; +import { listDemoSites, listSites } from "../hosting/api"; +import { getAppConfig, AppPlatform } from "../management/apps"; +import { promptOnce } from "../prompt"; +import { EmulatorInfo, Emulators, EMULATORS_SUPPORTED_BY_USE_EMULATOR } from "../emulator/types"; +import { getCredentialPathAsync } from "../defaultCredentials"; +import { getProjectDefaultAccount } from "../auth"; +import { formatHost } from "../emulator/functionsEmulatorShared"; +import { Constants } from "../emulator/constants"; +import { FirebaseError } from "../error"; +import { requireHostingSite } from "../requireHostingSite"; +import * as experiments from "../experiments"; +import { implicitInit } from "../hosting/implicitInit"; +import { + findDependency, + conjoinOptions, + frameworksCallToAction, + getFrameworksBuildTarget, +} from "./utils"; +import { + ALLOWED_SSR_REGIONS, + DEFAULT_REGION, + DEFAULT_SHOULD_USE_DEV_MODE_HANDLE, + FIREBASE_ADMIN_VERSION, + FIREBASE_FRAMEWORKS_VERSION, + FIREBASE_FUNCTIONS_VERSION, + GET_DEFAULT_BUILD_TARGETS, + I18N_ROOT, + NODE_VERSION, + SupportLevelWarnings, + VALID_ENGINES, +} from "./constants"; +import { + BUILD_TARGET_PURPOSE, + BuildResult, + FirebaseDefaults, + Framework, + FrameworkContext, + FrameworksOptions, +} from "./interfaces"; +import { logWarning } from "../utils"; +import { ensureTargeted } from "../functions/ensureTargeted"; +import { isDeepStrictEqual } from "util"; +import { resolveProjectPath } from "../projectPath"; +import { logger } from "../logger"; +import { WebFrameworks } from "./frameworks"; +import { constructDefaultWebSetup } from "../fetchWebSetup"; + +export { WebFrameworks }; + +/** + * + */ +export async function discover(dir: string, warn = true) { + const allFrameworkTypes = [ + ...new Set(Object.values(WebFrameworks).map(({ type }) => type)), + ].sort(); + for (const discoveryType of allFrameworkTypes) { + const frameworksDiscovered = []; + for (const framework in WebFrameworks) { + if (WebFrameworks[framework]) { + const { discover, type } = WebFrameworks[framework]; + if (type !== discoveryType) continue; + const result = await discover(dir); + if (result) frameworksDiscovered.push({ framework, ...result }); + } + } + if (frameworksDiscovered.length > 1) { + if (warn) console.error("Multiple conflicting frameworks discovered."); + return; + } + if (frameworksDiscovered.length === 1) return frameworksDiscovered[0]; + } + if (warn) console.warn("Could not determine the web framework in use."); + return; +} + +const BUILD_MEMO = new Map>(); + +// Memoize the build based on both the dir and the environment variables +function memoizeBuild( + dir: string, + build: Framework["build"], + deps: any[], + target: string, + context: FrameworkContext, +): ReturnType { + const key = [dir, ...deps]; + for (const existingKey of BUILD_MEMO.keys()) { + if (isDeepStrictEqual(existingKey, key)) { + return BUILD_MEMO.get(existingKey) as ReturnType; + } + } + const value = build(dir, target, context); + BUILD_MEMO.set(key, value); + return value; +} + +/** + * Use a function to ensure the same codebase name is used here and + * during hosting deploy. + */ +export function generateSSRCodebaseId(site: string) { + return `firebase-frameworks-${site}`; +} + +/** + * + */ +export async function prepareFrameworks( + purpose: BUILD_TARGET_PURPOSE, + targetNames: string[], + context: FrameworkContext | undefined, + options: FrameworksOptions, + emulators: EmulatorInfo[] = [], +): Promise { + const project = needProjectId(context || options); + const projectRoot = resolveProjectPath(options, "."); + const account = getProjectDefaultAccount(projectRoot); + // options.site is not present when emulated. We could call requireHostingSite but IAM permissions haven't + // been booted up (at this point) and we may be offline, so just use projectId. Most of the time + // the default site is named the same as the project & for frameworks this is only used for naming the + // function... unless you're using authenticated server-context TODO explore the implication here. + + // N.B. Trying to work around this in a rush but it's not 100% clear what to do here. + // The code previously injected a cache for the hosting options after specifying site: project + // temporarily in options. But that means we're caching configs with the wrong + // site specified. As a compromise we'll do our best to set the correct site, + // which should succeed when this method is being called from "deploy". I don't + // think this breaks any other situation because we don't need a site during + // emulation unless we have multiple sites, in which case we're guaranteed to + // either have site or target set. + if (!options.site) { + try { + await requireHostingSite(options); + } catch { + options.site = project; + } + } + const configs = hostingConfig(options); + let firebaseDefaults: FirebaseDefaults | undefined = undefined; + if (configs.length === 0) { + return; + } + const allowedRegionsValues = ALLOWED_SSR_REGIONS.map((r) => r.value); + for (const config of configs) { + const { source, site, public: publicDir, frameworksBackend } = config; + if (!source) { + continue; + } + config.rewrites ||= []; + config.redirects ||= []; + config.headers ||= []; + config.cleanUrls ??= true; + const dist = join(projectRoot, ".firebase", site); + const hostingDist = join(dist, "hosting"); + const functionsDist = join(dist, "functions"); + if (publicDir) { + throw new Error(`hosting.public and hosting.source cannot both be set in firebase.json`); + } + const ssrRegion = frameworksBackend?.region ?? DEFAULT_REGION; + const omitCloudFunction = frameworksBackend?.omit ?? false; + if (!allowedRegionsValues.includes(ssrRegion)) { + const validRegions = conjoinOptions(allowedRegionsValues); + throw new FirebaseError( + `Hosting config for site ${site} places server-side content in region ${ssrRegion} which is not known. Valid regions are ${validRegions}`, + ); + } + const getProjectPath = (...args: string[]) => join(projectRoot, source, ...args); + // Combined traffic tag (19 chars) and functionId cannot exceed 46 characters. + const functionId = `ssr${site.toLowerCase().replace(/-/g, "").substring(0, 20)}`; + const usesFirebaseAdminSdk = !!findDependency("firebase-admin", { cwd: getProjectPath() }); + const usesFirebaseJsSdk = !!findDependency("@firebase/app", { cwd: getProjectPath() }); + if (usesFirebaseAdminSdk) { + process.env.GOOGLE_CLOUD_PROJECT = project; + if (account && !process.env.GOOGLE_APPLICATION_CREDENTIALS) { + const defaultCredPath = await getCredentialPathAsync(account); + if (defaultCredPath) process.env.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath; + } + } + emulators.forEach((info) => { + if (usesFirebaseAdminSdk) { + if (info.name === Emulators.FIRESTORE) + process.env[Constants.FIRESTORE_EMULATOR_HOST] = formatHost(info); + if (info.name === Emulators.AUTH) + process.env[Constants.FIREBASE_AUTH_EMULATOR_HOST] = formatHost(info); + if (info.name === Emulators.DATABASE) + process.env[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = formatHost(info); + if (info.name === Emulators.STORAGE) + process.env[Constants.FIREBASE_STORAGE_EMULATOR_HOST] = formatHost(info); + } + if (usesFirebaseJsSdk && EMULATORS_SUPPORTED_BY_USE_EMULATOR.includes(info.name)) { + firebaseDefaults ||= {}; + firebaseDefaults.emulatorHosts ||= {}; + firebaseDefaults.emulatorHosts[info.name] = formatHost(info); + } + }); + let firebaseConfig = null; + if (usesFirebaseJsSdk) { + const isDemoProject = Constants.isDemoProject(project); + + const sites = isDemoProject ? listDemoSites(project) : await listSites(project); + const selectedSite = sites.find((it) => it.name && it.name.split("/").pop() === site); + if (selectedSite) { + const { appId } = selectedSite; + if (appId) { + firebaseConfig = isDemoProject + ? constructDefaultWebSetup(project) + : await getAppConfig(appId, AppPlatform.WEB); + firebaseDefaults ||= {}; + firebaseDefaults.config = firebaseConfig; + } else { + const defaultConfig = await implicitInit(options); + if (defaultConfig.json) { + console.warn( + `No Firebase app associated with site ${site}, injecting project default config. + You can link a Web app to a Hosting site here https://console.firebase.google.com/project/${project}/settings/general/web`, + ); + firebaseDefaults ||= {}; + firebaseDefaults.config = JSON.parse(defaultConfig.json); + } else { + // N.B. None of us know when this can ever happen and the deploy would + // still succeed. Maaaaybe if someone tried calling firebase serve + // on a project that never initialized hosting? + console.warn( + `No Firebase app associated with site ${site}, unable to provide authenticated server context. + You can link a Web app to a Hosting site here https://console.firebase.google.com/project/${project}/settings/general/web`, + ); + if (!options.nonInteractive) { + const continueDeploy = await promptOnce({ + type: "confirm", + default: true, + message: "Would you like to continue with the deploy?", + }); + if (!continueDeploy) exit(1); + } + } + } + } + } + if (firebaseDefaults) { + process.env.__FIREBASE_DEFAULTS__ = JSON.stringify(firebaseDefaults); + } + const results = await discover(getProjectPath()); + if (!results) { + throw new FirebaseError( + frameworksCallToAction( + "Unable to detect the web framework in use, check firebase-debug.log for more info.", + ), + ); + } + const { framework, mayWantBackend } = results; + const { + build, + ɵcodegenPublicDirectory, + ɵcodegenFunctionsDirectory: codegenProdModeFunctionsDirectory, + getDevModeHandle, + name, + support, + docsUrl, + supportedRange, + getValidBuildTargets = GET_DEFAULT_BUILD_TARGETS, + shouldUseDevModeHandle = DEFAULT_SHOULD_USE_DEV_MODE_HANDLE, + } = WebFrameworks[framework]; + + logger.info( + `\n${frameworksCallToAction( + SupportLevelWarnings[support](name), + docsUrl, + " ", + name, + results.version, + supportedRange, + results.vite, + )}\n`, + ); + + const hostingEmulatorInfo = emulators.find((e) => e.name === Emulators.HOSTING); + const validBuildTargets = await getValidBuildTargets(purpose, getProjectPath()); + const frameworksBuildTarget = getFrameworksBuildTarget(purpose, validBuildTargets); + const useDevModeHandle = + purpose !== "deploy" && + (await shouldUseDevModeHandle(frameworksBuildTarget, getProjectPath())); + + const frameworkContext: FrameworkContext = { + projectId: project, + site: options.site, + hostingChannel: context?.hostingChannel, + }; + + let codegenFunctionsDirectory: Framework["ɵcodegenFunctionsDirectory"]; + let baseUrl = ""; + const rewrites = []; + const redirects = []; + const headers = []; + + const devModeHandle = + useDevModeHandle && + getDevModeHandle && + (await getDevModeHandle(getProjectPath(), frameworksBuildTarget, hostingEmulatorInfo)); + if (devModeHandle) { + // Attach the handle to options, it will be used when spinning up superstatic + options.frameworksDevModeHandle = devModeHandle; + // null is the dev-mode entry for firebase-framework-tools + if (mayWantBackend && firebaseDefaults) { + codegenFunctionsDirectory = codegenDevModeFunctionsDirectory; + } + } else { + const buildResult = await memoizeBuild( + getProjectPath(), + build, + [firebaseDefaults, frameworksBuildTarget], + frameworksBuildTarget, + frameworkContext, + ); + const { wantsBackend = false, trailingSlash, i18n = false }: BuildResult = buildResult || {}; + + if (buildResult) { + baseUrl = buildResult.baseUrl ?? baseUrl; + if (buildResult.headers) headers.push(...buildResult.headers); + if (buildResult.rewrites) rewrites.push(...buildResult.rewrites); + if (buildResult.redirects) redirects.push(...buildResult.redirects); + } + + config.trailingSlash ??= trailingSlash; + if (i18n) config.i18n ??= { root: I18N_ROOT }; + + if (await pathExists(hostingDist)) await rm(hostingDist, { recursive: true }); + await mkdirp(hostingDist); + + await ɵcodegenPublicDirectory(getProjectPath(), hostingDist, frameworksBuildTarget, { + project, + site, + }); + + if (wantsBackend && !omitCloudFunction) + codegenFunctionsDirectory = codegenProdModeFunctionsDirectory; + } + config.public = relative(projectRoot, hostingDist); + config.webFramework = `${framework}${codegenFunctionsDirectory ? "_ssr" : ""}`; + if (codegenFunctionsDirectory) { + if (firebaseDefaults) { + firebaseDefaults._authTokenSyncURL = "/__session"; + process.env.__FIREBASE_DEFAULTS__ = JSON.stringify(firebaseDefaults); + } + + if (context?.hostingChannel) { + experiments.assertEnabled( + "pintags", + "deploy an app that requires a backend to a preview channel", + ); + } + + const codebase = generateSSRCodebaseId(site); + const existingFunctionsConfig = options.config.get("functions") + ? [].concat(options.config.get("functions")) + : []; + options.config.set("functions", [ + ...existingFunctionsConfig, + { + source: relative(projectRoot, functionsDist), + codebase, + }, + ]); + + // N.B. the pin-tags experiment already does this holistically later. + // This is just a fallback for previous behavior if the user manually + // disables the pintags experiment (e.g. there is a break and they would + // rather disable the experiment than roll back). + if (!experiments.isEnabled("pintags") || purpose !== "deploy") { + if (!targetNames.includes("functions")) { + targetNames.unshift("functions"); + } + if (options.only) { + options.only = ensureTargeted(options.only, codebase); + } + } + + // if exists, delete everything but the node_modules directory and package-lock.json + // this should speed up repeated NPM installs + if (await pathExists(functionsDist)) { + const functionsDistStat = await stat(functionsDist); + if (functionsDistStat?.isDirectory()) { + const files = await readdir(functionsDist); + for (const file of files) { + if (file !== "node_modules" && file !== "package-lock.json") + await rm(join(functionsDist, file), { recursive: true }); + } + } else { + await rm(functionsDist); + } + } else { + await mkdirp(functionsDist); + } + + const { + packageJson, + bootstrapScript, + frameworksEntry = framework, + dotEnv = {}, + rewriteSource, + } = await codegenFunctionsDirectory( + getProjectPath(), + functionsDist, + frameworksBuildTarget, + frameworkContext, + ); + + const rewrite = { + source: rewriteSource || posix.join(baseUrl, "**"), + function: { + functionId, + region: ssrRegion, + pinTag: experiments.isEnabled("pintags"), + }, + }; + + // If the rewriteSource is overridden, we're talking a very specific rewrite. E.g, Image Optimization + // in this case, we should ensure that it's the first priority—otherwise defer to the push/unshift + // logic based off the baseUrl + if (rewriteSource) { + config.rewrites.unshift(rewrite); + } else { + rewrites.push(rewrite); + } + + // Set the framework entry in the env variables to handle generation of the functions.yaml + process.env.__FIREBASE_FRAMEWORKS_ENTRY__ = frameworksEntry; + + packageJson.main = "server.js"; + packageJson.dependencies ||= {}; + packageJson.dependencies["firebase-frameworks"] ||= FIREBASE_FRAMEWORKS_VERSION; + packageJson.dependencies["firebase-functions"] ||= FIREBASE_FUNCTIONS_VERSION; + packageJson.dependencies["firebase-admin"] ||= FIREBASE_ADMIN_VERSION; + packageJson.engines ||= {}; + const validEngines = VALID_ENGINES.node.filter((it) => it <= NODE_VERSION); + const engine = validEngines[validEngines.length - 1] || VALID_ENGINES.node[0]; + if (engine !== NODE_VERSION) { + logWarning( + `This integration expects Node version ${conjoinOptions( + VALID_ENGINES.node, + "or", + )}. You're running version ${NODE_VERSION}, problems may be encountered.`, + ); + } + packageJson.engines.node ||= engine.toString(); + delete packageJson.scripts; + delete packageJson.devDependencies; + + const bundledDependencies: Record = packageJson.bundledDependencies || {}; + if (Object.keys(bundledDependencies).length) { + logWarning( + "Bundled dependencies aren't supported in Cloud Functions, converting to dependencies.", + ); + for (const [dep, version] of Object.entries(bundledDependencies)) { + packageJson.dependencies[dep] ||= version; + } + delete packageJson.bundledDependencies; + } + + for (const [name, version] of Object.entries( + packageJson.dependencies as Record, + )) { + if (version.startsWith("file:")) { + const path = version.replace(/^file:/, ""); + if (!(await pathExists(path))) continue; + const stats = await stat(path); + if (stats.isDirectory()) { + const result = spawnSync( + "npm", + ["pack", relative(functionsDist, path), "--json=true"], + { + cwd: functionsDist, + }, + ); + if (result.status !== 0) + throw new FirebaseError(`Error running \`npm pack\` at ${path}`); + const { filename } = JSON.parse(result.stdout.toString())[0]; + packageJson.dependencies[name] = `file:${filename}`; + } else { + const filename = basename(path); + await copyFile(path, join(functionsDist, filename)); + packageJson.dependencies[name] = `file:${filename}`; + } + } + } + await writeFile(join(functionsDist, "package.json"), JSON.stringify(packageJson, null, 2)); + + await copyFile( + getProjectPath("package-lock.json"), + join(functionsDist, "package-lock.json"), + ).catch(() => { + // continue + }); + + if (await pathExists(getProjectPath(".npmrc"))) { + await copyFile(getProjectPath(".npmrc"), join(functionsDist, ".npmrc")); + } + + let dotEnvContents = ""; + if (await pathExists(getProjectPath(".env"))) { + dotEnvContents = (await readFile(getProjectPath(".env"))).toString(); + } + + for (const [key, value] of Object.entries(dotEnv)) { + dotEnvContents += `\n${key}=${value}`; + } + + await writeFile( + join(functionsDist, ".env"), + `${dotEnvContents} +__FIREBASE_FRAMEWORKS_ENTRY__=${frameworksEntry} +${ + firebaseDefaults ? `__FIREBASE_DEFAULTS__=${JSON.stringify(firebaseDefaults)}\n` : "" +}`.trimStart(), + ); + + const envs = await glob(getProjectPath(".env.*")); + + await Promise.all(envs.map((path) => copyFile(path, join(functionsDist, basename(path))))); + + execSync(`npm i --omit dev --no-audit`, { + cwd: functionsDist, + stdio: "inherit", + }); + + if (bootstrapScript) await writeFile(join(functionsDist, "bootstrap.js"), bootstrapScript); + + // TODO move to templates + + if (packageJson.type === "module") { + await writeFile( + join(functionsDist, "server.js"), + `import { onRequest } from 'firebase-functions/v2/https'; + const server = import('firebase-frameworks'); + export const ${functionId} = onRequest(${JSON.stringify( + frameworksBackend || {}, + )}, (req, res) => server.then(it => it.handle(req, res))); + `, + ); + } else { + await writeFile( + join(functionsDist, "server.js"), + `const { onRequest } = require('firebase-functions/v2/https'); + const server = import('firebase-frameworks'); + exports.${functionId} = onRequest(${JSON.stringify( + frameworksBackend || {}, + )}, (req, res) => server.then(it => it.handle(req, res))); + `, + ); + } + } else { + if (await pathExists(functionsDist)) { + await rm(functionsDist, { recursive: true }); + } + } + + const ourConfigShouldComeFirst = !["", "/"].includes(baseUrl); + const operation = ourConfigShouldComeFirst ? "unshift" : "push"; + + config.rewrites[operation](...rewrites); + config.redirects[operation](...redirects); + config.headers[operation](...headers); + + if (firebaseDefaults) { + const encodedDefaults = Buffer.from(JSON.stringify(firebaseDefaults)).toString("base64url"); + const expires = new Date(new Date().getTime() + 60_000_000_000); + const sameSite = "Strict"; + const path = `/`; + config.headers.push({ + source: posix.join(baseUrl, "**", "*.[jt]s"), + headers: [ + { + key: "Set-Cookie", + value: `__FIREBASE_DEFAULTS__=${encodedDefaults}; SameSite=${sameSite}; Expires=${expires.toISOString()}; Path=${path};`, + }, + ], + }); + } + } + + logger.debug( + "[web frameworks] effective firebase.json: ", + JSON.stringify({ hosting: configs, functions: options.config.get("functions") }, undefined, 2), + ); + + // Clean up memos/caches + BUILD_MEMO.clear(); + + // Clean up ENV variables, if were emulatoring .env won't override + // this is leads to failures if we're hosting multiple sites + delete process.env.__FIREBASE_DEFAULTS__; + delete process.env.__FIREBASE_FRAMEWORKS_ENTRY__; +} + +function codegenDevModeFunctionsDirectory() { + const packageJson = {}; + return Promise.resolve({ packageJson, frameworksEntry: "_devMode" }); +} diff --git a/src/frameworks/interfaces.ts b/src/frameworks/interfaces.ts new file mode 100644 index 00000000000..51344e751fc --- /dev/null +++ b/src/frameworks/interfaces.ts @@ -0,0 +1,109 @@ +import { IncomingMessage, ServerResponse } from "http"; +import { EmulatorInfo } from "../emulator/types"; +import { HostingHeaders, HostingRedirects, HostingRewrites } from "../firebaseConfig"; +import { HostingOptions } from "../hosting/options"; +import { Options } from "../options"; + +// These serve as the order of operations for discovery +// E.g, a framework utilizing Vite should be given priority +// over the vite tooling +export const enum FrameworkType { + Custom = 0, // express + Monorep, // nx, lerna + MetaFramework, // next.js, nest.js + Framework, // angular, react + Toolchain, // vite +} + +export const enum SupportLevel { + Experimental = "experimental", + Preview = "preview", +} + +export interface Discovery { + mayWantBackend: boolean; + version?: string; + vite?: boolean; +} + +export interface BuildResult { + rewrites?: HostingRewrites[]; + redirects?: HostingRedirects[]; + headers?: HostingHeaders[]; + wantsBackend?: boolean; + trailingSlash?: boolean; + i18n?: boolean; + baseUrl?: string; +} + +export type RequestHandler = ( + req: IncomingMessage, + res: ServerResponse, + next: () => void, +) => void | Promise; + +export type FrameworksOptions = HostingOptions & + Options & { + frameworksDevModeHandle?: RequestHandler; + nonInteractive?: boolean; + }; + +export type FrameworkContext = { + projectId?: string; + hostingChannel?: string; + site?: string; +}; + +export interface Framework { + supportedRange?: string; + discover: (dir: string) => Promise; + type: FrameworkType; + name: string; + build: (dir: string, target: string, context?: FrameworkContext) => Promise; + support: SupportLevel; + docsUrl?: string; + init?: (setup: any, config: any) => Promise; + getDevModeHandle?: ( + dir: string, + target: string, + hostingEmulatorInfo?: EmulatorInfo, + ) => Promise; + ɵcodegenPublicDirectory: ( + dir: string, + dest: string, + target: string, + context: { + project: string; + site: string; + }, + ) => Promise; + ɵcodegenFunctionsDirectory?: ( + dir: string, + dest: string, + target: string, + context?: FrameworkContext, + ) => Promise<{ + bootstrapScript?: string; + packageJson: any; + frameworksEntry?: string; + dotEnv?: Record; + rewriteSource?: string; + }>; + getValidBuildTargets?: (purpose: BUILD_TARGET_PURPOSE, dir: string) => Promise; + shouldUseDevModeHandle?: (target: string, dir: string) => Promise; +} + +export type BUILD_TARGET_PURPOSE = "deploy" | "test" | "emulate"; + +// TODO pull from @firebase/util when published +export interface FirebaseDefaults { + config?: Object; + emulatorHosts?: Record; + _authTokenSyncURL?: string; +} + +// Only the fields being used are defined here +export interface PackageJson { + main: string; + type?: "commonjs" | "module"; +} diff --git a/src/frameworks/lit/index.ts b/src/frameworks/lit/index.ts new file mode 100644 index 00000000000..2130130b3a3 --- /dev/null +++ b/src/frameworks/lit/index.ts @@ -0,0 +1,10 @@ +import { FrameworkType } from "../interfaces"; +import { initViteTemplate, viteDiscoverWithNpmDependency } from "../vite"; + +export * from "../vite"; + +export const name = "Lit"; +export const type = FrameworkType.Framework; + +export const init = initViteTemplate("lit"); +export const discover = viteDiscoverWithNpmDependency("lit"); diff --git a/src/frameworks/next/constants.ts b/src/frameworks/next/constants.ts new file mode 100644 index 00000000000..bb486212cb2 --- /dev/null +++ b/src/frameworks/next/constants.ts @@ -0,0 +1,89 @@ +import type { + APP_PATH_ROUTES_MANIFEST as APP_PATH_ROUTES_MANIFEST_TYPE, + EXPORT_MARKER as EXPORT_MARKER_TYPE, + IMAGES_MANIFEST as IMAGES_MANIFEST_TYPE, + MIDDLEWARE_MANIFEST as MIDDLEWARE_MANIFEST_TYPE, + PAGES_MANIFEST as PAGES_MANIFEST_TYPE, + PRERENDER_MANIFEST as PRERENDER_MANIFEST_TYPE, + ROUTES_MANIFEST as ROUTES_MANIFEST_TYPE, + APP_PATHS_MANIFEST as APP_PATHS_MANIFEST_TYPE, + SERVER_REFERENCE_MANIFEST as SERVER_REFERENCE_MANIFEST_TYPE, +} from "next/constants"; +import type { WEBPACK_LAYERS as NEXTJS_WEBPACK_LAYERS } from "next/dist/lib/constants"; + +export const APP_PATH_ROUTES_MANIFEST: typeof APP_PATH_ROUTES_MANIFEST_TYPE = + "app-path-routes-manifest.json"; +export const EXPORT_MARKER: typeof EXPORT_MARKER_TYPE = "export-marker.json"; +export const IMAGES_MANIFEST: typeof IMAGES_MANIFEST_TYPE = "images-manifest.json"; +export const MIDDLEWARE_MANIFEST: typeof MIDDLEWARE_MANIFEST_TYPE = "middleware-manifest.json"; +export const PAGES_MANIFEST: typeof PAGES_MANIFEST_TYPE = "pages-manifest.json"; +export const PRERENDER_MANIFEST: typeof PRERENDER_MANIFEST_TYPE = "prerender-manifest.json"; +export const ROUTES_MANIFEST: typeof ROUTES_MANIFEST_TYPE = "routes-manifest.json"; +export const APP_PATHS_MANIFEST: typeof APP_PATHS_MANIFEST_TYPE = "app-paths-manifest.json"; +export const SERVER_REFERENCE_MANIFEST: `${typeof SERVER_REFERENCE_MANIFEST_TYPE}.json` = + "server-reference-manifest.json"; + +export const CONFIG_FILES = ["next.config.js", "next.config.mjs"] as const; + +export const ESBUILD_VERSION = "0.19.2"; + +// This is copied from Next.js source code to keep WEBPACK_LAYERS in sync with the Next.js definition. +const WEBPACK_LAYERS_NAMES = { + /** + * The layer for the shared code between the client and server bundles. + */ shared: "shared", + /** + * React Server Components layer (rsc). + */ reactServerComponents: "rsc", + /** + * Server Side Rendering layer for app (ssr). + */ serverSideRendering: "ssr", + /** + * The browser client bundle layer for actions. + */ actionBrowser: "action-browser", + /** + * The layer for the API routes. + */ api: "api", + /** + * The layer for the middleware code. + */ middleware: "middleware", + /** + * The layer for assets on the edge. + */ edgeAsset: "edge-asset", + /** + * The browser client bundle layer for App directory. + */ appPagesBrowser: "app-pages-browser", + /** + * The server bundle layer for metadata routes. + */ appMetadataRoute: "app-metadata-route", + /** + * The layer for the server bundle for App Route handlers. + */ appRouteHandler: "app-route-handler", +} as const; + +// This is copied from Next.js source code to keep WEBPACK_LAYERS in sync with the Next.js definition. +export const WEBPACK_LAYERS: typeof NEXTJS_WEBPACK_LAYERS = { + ...WEBPACK_LAYERS_NAMES, + GROUP: { + server: [ + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + WEBPACK_LAYERS_NAMES.appMetadataRoute, + WEBPACK_LAYERS_NAMES.appRouteHandler, + ], + nonClientServerTarget: [ + // plus middleware and pages api + WEBPACK_LAYERS_NAMES.middleware, + WEBPACK_LAYERS_NAMES.api, + ], + app: [ + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + WEBPACK_LAYERS_NAMES.appMetadataRoute, + WEBPACK_LAYERS_NAMES.appRouteHandler, + WEBPACK_LAYERS_NAMES.serverSideRendering, + WEBPACK_LAYERS_NAMES.appPagesBrowser, + WEBPACK_LAYERS_NAMES.shared, + ], + }, +}; diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts new file mode 100644 index 00000000000..b74040e2e56 --- /dev/null +++ b/src/frameworks/next/index.ts @@ -0,0 +1,732 @@ +import { execSync } from "child_process"; +import { spawn, sync as spawnSync } from "cross-spawn"; +import { mkdir, copyFile } from "fs/promises"; +import { basename, dirname, join } from "path"; +import type { NextConfig } from "next"; +import type { PrerenderManifest } from "next/dist/build"; +import type { DomainLocale } from "next/dist/server/config"; +import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin"; +import { copy, mkdirp, pathExists, pathExistsSync } from "fs-extra"; +import { pathToFileURL, parse } from "url"; +import { gte } from "semver"; +import { IncomingMessage, ServerResponse } from "http"; +import * as clc from "colorette"; +import { chain } from "stream-chain"; +import { parser } from "stream-json"; +import { pick } from "stream-json/filters/Pick"; +import { streamObject } from "stream-json/streamers/StreamObject"; +import { fileExistsSync } from "../../fsutils"; + +import { promptOnce } from "../../prompt"; +import { FirebaseError } from "../../error"; +import type { EmulatorInfo } from "../../emulator/types"; +import { + readJSON, + simpleProxy, + warnIfCustomBuildScript, + relativeRequire, + findDependency, + validateLocales, + getNodeModuleBin, +} from "../utils"; +import { + BuildResult, + Framework, + FrameworkContext, + FrameworkType, + SupportLevel, +} from "../interfaces"; + +import { + cleanEscapedChars, + getNextjsRewritesToUse, + isHeaderSupportedByHosting, + isRedirectSupportedByHosting, + isRewriteSupportedByHosting, + isUsingImageOptimization, + isUsingMiddleware, + allDependencyNames, + getMiddlewareMatcherRegexes, + getNonStaticRoutes, + getNonStaticServerComponents, + getHeadersFromMetaFiles, + cleanI18n, + getNextVersion, + hasStaticAppNotFoundComponent, + getRoutesWithServerAction, + getProductionDistDirFiles, + whichNextConfigFile, +} from "./utils"; +import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES, SHARP_VERSION, I18N_ROOT } from "../constants"; +import type { + AppPathRoutesManifest, + AppPathsManifest, + HostingHeadersWithSource, + RoutesManifest, + NpmLsDepdendency, + MiddlewareManifest, + ActionManifest, +} from "./interfaces"; +import { + MIDDLEWARE_MANIFEST, + PAGES_MANIFEST, + PRERENDER_MANIFEST, + ROUTES_MANIFEST, + APP_PATH_ROUTES_MANIFEST, + APP_PATHS_MANIFEST, + ESBUILD_VERSION, + SERVER_REFERENCE_MANIFEST, +} from "./constants"; +import { getAllSiteDomains, getDeploymentDomain } from "../../hosting/api"; +import { logger } from "../../logger"; + +const DEFAULT_BUILD_SCRIPT = ["next build"]; +const PUBLIC_DIR = "public"; + +export const supportedRange = "12 - 14.0"; + +export const name = "Next.js"; +export const support = SupportLevel.Preview; +export const type = FrameworkType.MetaFramework; +export const docsUrl = "https://firebase.google.com/docs/hosting/frameworks/nextjs"; + +const BUNDLE_NEXT_CONFIG_TIMEOUT = 60_000; +const DEFAULT_NUMBER_OF_REASONS_TO_LIST = 5; + +function getReactVersion(cwd: string): string | undefined { + return findDependency("react-dom", { cwd, omitDev: false })?.version; +} + +/** + * Returns whether this codebase is a Next.js backend. + */ +export async function discover(dir: string) { + if (!(await pathExists(join(dir, "package.json")))) return; + const version = getNextVersion(dir); + if (!(await whichNextConfigFile(dir)) && !version) return; + + return { mayWantBackend: true, publicDirectory: join(dir, PUBLIC_DIR), version }; +} + +/** + * Build a next.js application. + */ +export async function build( + dir: string, + target: string, + context?: FrameworkContext, +): Promise { + await warnIfCustomBuildScript(dir, name, DEFAULT_BUILD_SCRIPT); + + const reactVersion = getReactVersion(dir); + if (reactVersion && gte(reactVersion, "18.0.0")) { + // This needs to be set for Next build to succeed with React 18 + process.env.__NEXT_REACT_ROOT = "true"; + } + + const env = { ...process.env }; + + if (context?.projectId && context?.site) { + const deploymentDomain = await getDeploymentDomain( + context.projectId, + context.site, + context.hostingChannel, + ); + + if (deploymentDomain) { + // Add the deployment domain to VERCEL_URL env variable, which is + // required for dynamic OG images to work without manual configuration. + // See: https://nextjs.org/docs/app/api-reference/functions/generate-metadata#default-value + env["VERCEL_URL"] = deploymentDomain; + } + } + + const cli = getNodeModuleBin("next", dir); + + const nextBuild = new Promise((resolve, reject) => { + const buildProcess = spawn(cli, ["build"], { cwd: dir, env }); + buildProcess.stdout?.on("data", (data) => logger.info(data.toString())); + buildProcess.stderr?.on("data", (data) => logger.info(data.toString())); + buildProcess.on("error", (err) => { + reject(new FirebaseError(`Unable to build your Next.js app: ${err}`)); + }); + buildProcess.on("exit", (code) => { + resolve(code); + }); + }); + await nextBuild; + + const reasonsForBackend = new Set(); + const { distDir, trailingSlash, basePath: baseUrl } = await getConfig(dir); + + if (await isUsingMiddleware(join(dir, distDir), false)) { + reasonsForBackend.add("middleware"); + } + + if (await isUsingImageOptimization(dir, distDir)) { + reasonsForBackend.add(`Image Optimization`); + } + + const prerenderManifest = await readJSON( + join(dir, distDir, PRERENDER_MANIFEST), + ); + + const dynamicRoutesWithFallback = Object.entries(prerenderManifest.dynamicRoutes || {}).filter( + ([, it]) => it.fallback !== false, + ); + if (dynamicRoutesWithFallback.length > 0) { + for (const [key] of dynamicRoutesWithFallback) { + reasonsForBackend.add(`use of fallback ${key}`); + } + } + + const routesWithRevalidate = Object.entries(prerenderManifest.routes).filter( + ([, it]) => it.initialRevalidateSeconds, + ); + if (routesWithRevalidate.length > 0) { + for (const [, { srcRoute }] of routesWithRevalidate) { + reasonsForBackend.add(`use of revalidate ${srcRoute}`); + } + } + + const pagesManifestJSON = await readJSON( + join(dir, distDir, "server", PAGES_MANIFEST), + ); + const prerenderedRoutes = Object.keys(prerenderManifest.routes); + const dynamicRoutes = Object.keys(prerenderManifest.dynamicRoutes); + + const unrenderedPages = getNonStaticRoutes(pagesManifestJSON, prerenderedRoutes, dynamicRoutes); + + for (const key of unrenderedPages) { + reasonsForBackend.add(`non-static route ${key}`); + } + + const manifest = await readJSON(join(dir, distDir, ROUTES_MANIFEST)); + + const { + headers: nextJsHeaders = [], + redirects: nextJsRedirects = [], + rewrites: nextJsRewrites = [], + i18n: nextjsI18n, + } = manifest; + + const isEveryHeaderSupported = nextJsHeaders.map(cleanI18n).every(isHeaderSupportedByHosting); + if (!isEveryHeaderSupported) { + reasonsForBackend.add("advanced headers"); + } + + const headers: HostingHeadersWithSource[] = nextJsHeaders + .map(cleanI18n) + .filter(isHeaderSupportedByHosting) + .map(({ source, headers }) => ({ + // clean up unnecessary escaping + source: cleanEscapedChars(source), + headers, + })); + + const [appPathsManifest, appPathRoutesManifest, serverReferenceManifest] = await Promise.all([ + readJSON(join(dir, distDir, "server", APP_PATHS_MANIFEST)).catch( + () => undefined, + ), + readJSON(join(dir, distDir, APP_PATH_ROUTES_MANIFEST)).catch( + () => undefined, + ), + readJSON(join(dir, distDir, "server", SERVER_REFERENCE_MANIFEST)).catch( + () => undefined, + ), + ]); + + if (appPathRoutesManifest) { + const headersFromMetaFiles = await getHeadersFromMetaFiles( + dir, + distDir, + baseUrl, + appPathRoutesManifest, + ); + headers.push(...headersFromMetaFiles); + + if (appPathsManifest) { + const unrenderedServerComponents = getNonStaticServerComponents( + appPathsManifest, + appPathRoutesManifest, + prerenderedRoutes, + dynamicRoutes, + ); + + const notFoundPageKey = ["/_not-found", "/_not-found/page"].find((key) => + unrenderedServerComponents.has(key), + ); + if (notFoundPageKey && (await hasStaticAppNotFoundComponent(dir, distDir))) { + unrenderedServerComponents.delete(notFoundPageKey); + } + + for (const key of unrenderedServerComponents) { + reasonsForBackend.add(`non-static component ${key}`); + } + } + + if (serverReferenceManifest) { + const routesWithServerAction = getRoutesWithServerAction( + serverReferenceManifest, + appPathRoutesManifest, + ); + + for (const key of routesWithServerAction) { + reasonsForBackend.add(`route with server action ${key}`); + } + } + } + + const isEveryRedirectSupported = nextJsRedirects + .filter((it) => !it.internal) + .every(isRedirectSupportedByHosting); + if (!isEveryRedirectSupported) { + reasonsForBackend.add("advanced redirects"); + } + + const redirects = nextJsRedirects + .map(cleanI18n) + .filter(isRedirectSupportedByHosting) + .map(({ source, destination, statusCode: type }) => ({ + // clean up unnecessary escaping + source: cleanEscapedChars(source), + destination, + type, + })); + + const nextJsRewritesToUse = getNextjsRewritesToUse(nextJsRewrites); + + // rewrites.afterFiles / rewrites.fallback are not supported by firebase.json + if ( + !Array.isArray(nextJsRewrites) && + (nextJsRewrites.afterFiles?.length || nextJsRewrites.fallback?.length) + ) { + reasonsForBackend.add("advanced rewrites"); + } + + const isEveryRewriteSupported = nextJsRewritesToUse.every(isRewriteSupportedByHosting); + if (!isEveryRewriteSupported) { + reasonsForBackend.add("advanced rewrites"); + } + + const rewrites = nextJsRewritesToUse + .filter(isRewriteSupportedByHosting) + .map(cleanI18n) + .map(({ source, destination }) => ({ + // clean up unnecessary escaping + source: cleanEscapedChars(source), + destination, + })); + + const wantsBackend = reasonsForBackend.size > 0; + + if (wantsBackend) { + logger.info("Building a Cloud Function to run this application. This is needed due to:"); + for (const reason of Array.from(reasonsForBackend).slice( + 0, + DEFAULT_NUMBER_OF_REASONS_TO_LIST, + )) { + logger.info(` • ${reason}`); + } + for (const reason of Array.from(reasonsForBackend).slice(DEFAULT_NUMBER_OF_REASONS_TO_LIST)) { + logger.debug(` • ${reason}`); + } + if (reasonsForBackend.size > DEFAULT_NUMBER_OF_REASONS_TO_LIST && !process.env.DEBUG) { + logger.info( + ` • and ${ + reasonsForBackend.size - DEFAULT_NUMBER_OF_REASONS_TO_LIST + } other reasons, use --debug to see more`, + ); + } + logger.info(""); + } + + const i18n = !!nextjsI18n; + + return { + wantsBackend, + headers, + redirects, + rewrites, + trailingSlash, + i18n, + baseUrl, + }; +} + +/** + * Utility method used during project initialization. + */ +export async function init(setup: any, config: any) { + const language = await promptOnce({ + type: "list", + default: "TypeScript", + message: "What language would you like to use?", + choices: ["JavaScript", "TypeScript"], + }); + execSync( + `npx --yes create-next-app@"${supportedRange}" -e hello-world ${ + setup.hosting.source + } --use-npm ${language === "TypeScript" ? "--ts" : "--js"}`, + { stdio: "inherit", cwd: config.projectDir }, + ); +} + +/** + * Create a directory for SSG content. + */ +export async function ɵcodegenPublicDirectory( + sourceDir: string, + destDir: string, + _: string, + context: { site: string; project: string }, +) { + const { distDir, i18n, basePath } = await getConfig(sourceDir); + + let matchingI18nDomain: DomainLocale | undefined = undefined; + if (i18n?.domains) { + const siteDomains = await getAllSiteDomains(context.project, context.site); + matchingI18nDomain = i18n.domains.find(({ domain }) => siteDomains.includes(domain)); + } + const singleLocaleDomain = !i18n || ((matchingI18nDomain || i18n).locales || []).length <= 1; + + const publicPath = join(sourceDir, "public"); + await mkdir(join(destDir, basePath, "_next", "static"), { recursive: true }); + if (await pathExists(publicPath)) { + await copy(publicPath, join(destDir, basePath)); + } + await copy(join(sourceDir, distDir, "static"), join(destDir, basePath, "_next", "static")); + + const [ + middlewareManifest, + prerenderManifest, + routesManifest, + pagesManifest, + appPathRoutesManifest, + serverReferenceManifest, + ] = await Promise.all([ + readJSON(join(sourceDir, distDir, "server", MIDDLEWARE_MANIFEST)), + readJSON(join(sourceDir, distDir, PRERENDER_MANIFEST)), + readJSON(join(sourceDir, distDir, ROUTES_MANIFEST)), + readJSON(join(sourceDir, distDir, "server", PAGES_MANIFEST)), + readJSON(join(sourceDir, distDir, APP_PATH_ROUTES_MANIFEST)).catch( + () => ({}), + ), + readJSON(join(sourceDir, distDir, "server", SERVER_REFERENCE_MANIFEST)).catch( + () => ({ node: {}, edge: {}, encryptionKey: "" }), + ), + ]); + + const appPathRoutesEntries = Object.entries(appPathRoutesManifest); + + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareManifest); + + const { redirects = [], rewrites = [], headers = [] } = routesManifest; + + const rewritesRegexesNotSupportedByHosting = getNextjsRewritesToUse(rewrites) + .filter((rewrite) => !isRewriteSupportedByHosting(rewrite)) + .map(cleanI18n) + .map((rewrite) => new RegExp(rewrite.regex)); + + const redirectsRegexesNotSupportedByHosting = redirects + .filter((it) => !it.internal) + .filter((redirect) => !isRedirectSupportedByHosting(redirect)) + .map(cleanI18n) + .map((redirect) => new RegExp(redirect.regex)); + + const headersRegexesNotSupportedByHosting = headers + .filter((header) => !isHeaderSupportedByHosting(header)) + .map((header) => new RegExp(header.regex)); + + const pathsUsingsFeaturesNotSupportedByHosting = [ + ...middlewareMatcherRegexes, + ...rewritesRegexesNotSupportedByHosting, + ...redirectsRegexesNotSupportedByHosting, + ...headersRegexesNotSupportedByHosting, + ]; + + const staticRoutesUsingServerActions = getRoutesWithServerAction( + serverReferenceManifest, + appPathRoutesManifest, + ); + + const pagesManifestLikePrerender: PrerenderManifest["routes"] = Object.fromEntries( + Object.entries(pagesManifest) + .filter(([, srcRoute]) => srcRoute.endsWith(".html")) + .map(([path]) => { + return [ + path, + { + srcRoute: null, + initialRevalidateSeconds: false, + dataRoute: "", + experimentalPPR: false, + prefetchDataRoute: "", + }, + ]; + }), + ); + + const routesToCopy: PrerenderManifest["routes"] = { + ...prerenderManifest.routes, + ...pagesManifestLikePrerender, + }; + + await Promise.all( + Object.entries(routesToCopy).map(async ([path, route]) => { + if (route.initialRevalidateSeconds) { + logger.debug(`skipping ${path} due to revalidate`); + return; + } + if (pathsUsingsFeaturesNotSupportedByHosting.some((it) => path.match(it))) { + logger.debug( + `skipping ${path} due to it matching an unsupported rewrite/redirect/header or middlware`, + ); + return; + } + + if (staticRoutesUsingServerActions.some((it) => path === it)) { + logger.debug(`skipping ${path} due to server action`); + return; + } + + const appPathRoute = + route.srcRoute && appPathRoutesEntries.find(([, it]) => it === route.srcRoute)?.[0]; + const contentDist = join(sourceDir, distDir, "server", appPathRoute ? "app" : "pages"); + + const sourceParts = path.split("/").filter((it) => !!it); + const locale = i18n?.locales.includes(sourceParts[0]) ? sourceParts[0] : undefined; + const includeOnThisDomain = + !locale || + !matchingI18nDomain || + matchingI18nDomain.defaultLocale === locale || + !matchingI18nDomain.locales || + matchingI18nDomain.locales.includes(locale); + + if (!includeOnThisDomain) { + logger.debug(`skipping ${path} since it is for a locale not deployed on this domain`); + return; + } + + const sourcePartsOrIndex = sourceParts.length > 0 ? sourceParts : ["index"]; + const destParts = sourceParts.slice(locale ? 1 : 0); + const destPartsOrIndex = destParts.length > 0 ? destParts : ["index"]; + const isDefaultLocale = !locale || (matchingI18nDomain || i18n)?.defaultLocale === locale; + + let sourcePath = join(contentDist, ...sourcePartsOrIndex); + let localizedDestPath = + !singleLocaleDomain && + locale && + join(destDir, I18N_ROOT, locale, basePath, ...destPartsOrIndex); + let defaultDestPath = isDefaultLocale && join(destDir, basePath, ...destPartsOrIndex); + if (!fileExistsSync(sourcePath) && fileExistsSync(`${sourcePath}.html`)) { + sourcePath += ".html"; + if (localizedDestPath) localizedDestPath += ".html"; + if (defaultDestPath) defaultDestPath += ".html"; + } else if ( + appPathRoute && + basename(appPathRoute) === "route" && + fileExistsSync(`${sourcePath}.body`) + ) { + sourcePath += ".body"; + } else if (!pathExistsSync(sourcePath)) { + console.error(`Cannot find ${path} in your compiled Next.js application.`); + return; + } + + if (localizedDestPath) { + await mkdir(dirname(localizedDestPath), { recursive: true }); + await copyFile(sourcePath, localizedDestPath); + } + + if (defaultDestPath) { + await mkdir(dirname(defaultDestPath), { recursive: true }); + await copyFile(sourcePath, defaultDestPath); + } + + if (route.dataRoute && !appPathRoute) { + const dataSourcePath = `${join(...sourcePartsOrIndex)}.json`; + const dataDestPath = join(destDir, basePath, route.dataRoute); + await mkdir(dirname(dataDestPath), { recursive: true }); + await copyFile(join(contentDist, dataSourcePath), dataDestPath); + } + }), + ); +} + +/** + * Create a directory for SSR content. + */ +export async function ɵcodegenFunctionsDirectory( + sourceDir: string, + destDir: string, + target: string, + context?: FrameworkContext, +): ReturnType> { + const { distDir } = await getConfig(sourceDir); + const packageJson = await readJSON(join(sourceDir, "package.json")); + // Bundle their next.config.js with esbuild via NPX, pinned version was having troubles on m1 + // macs and older Node versions; either way, we should avoid taking on any deps in firebase-tools + // Alternatively I tried using @swc/spack and the webpack bundled into Next.js but was + // encountering difficulties with both of those + const configFile = await whichNextConfigFile(sourceDir); + if (configFile) { + try { + const productionDeps = await new Promise((resolve) => { + const dependencies: string[] = []; + const npmLs = spawn("npm", ["ls", "--omit=dev", "--all", "--json=true"], { + cwd: sourceDir, + timeout: NPM_COMMAND_TIMEOUT_MILLIES, + }); + const pipeline = chain([ + npmLs.stdout, + parser({ packValues: false, packKeys: true, streamValues: false }), + pick({ filter: "dependencies" }), + streamObject(), + ({ key, value }: { key: string; value: NpmLsDepdendency }) => [ + key, + ...allDependencyNames(value), + ], + ]); + pipeline.on("data", (it: string) => dependencies.push(it)); + pipeline.on("end", () => { + resolve([...new Set(dependencies)]); + }); + }); + // Mark all production deps as externals, so they aren't bundled + // DevDeps won't be included in the Cloud Function, so they should be bundled + const esbuildArgs = productionDeps + .map((it) => `--external:${it}`) + .concat("--bundle", "--platform=node", `--target=node${NODE_VERSION}`, "--log-level=error"); + + if (configFile === "next.config.mjs") { + // ensure generated file is .mjs if the config is .mjs + esbuildArgs.push(...[`--outfile=${join(destDir, configFile)}`, "--format=esm"]); + } else { + esbuildArgs.push(`--outfile=${join(destDir, configFile)}`); + } + + const bundle = spawnSync( + "npx", + ["--yes", `esbuild@${ESBUILD_VERSION}`, configFile, ...esbuildArgs], + { + cwd: sourceDir, + timeout: BUNDLE_NEXT_CONFIG_TIMEOUT, + }, + ); + if (bundle.status !== 0) { + throw new FirebaseError(bundle.stderr.toString()); + } + } catch (e: any) { + console.warn( + `Unable to bundle ${configFile} for use in Cloud Functions, proceeding with deploy but problems may be encountered.`, + ); + console.error(e.message || e); + await copy(join(sourceDir, configFile), join(destDir, configFile)); + } + } + if (await pathExists(join(sourceDir, "public"))) { + await mkdir(join(destDir, "public")); + await copy(join(sourceDir, "public"), join(destDir, "public")); + } + + // Add the `sharp` library if app is using image optimization + if (await isUsingImageOptimization(sourceDir, distDir)) { + packageJson.dependencies["sharp"] = SHARP_VERSION; + } + + const dotEnv: Record = {}; + if (context?.projectId && context?.site) { + const deploymentDomain = await getDeploymentDomain( + context.projectId, + context.site, + context.hostingChannel, + ); + + if (deploymentDomain) { + // Add the deployment domain to VERCEL_URL env variable, which is + // required for dynamic OG images to work without manual configuration. + // See: https://nextjs.org/docs/app/api-reference/functions/generate-metadata#default-value + dotEnv["VERCEL_URL"] = deploymentDomain; + } + } + + const [productionDistDirfiles] = await Promise.all([ + getProductionDistDirFiles(sourceDir, distDir), + mkdirp(join(destDir, distDir)), + ]); + + await Promise.all( + productionDistDirfiles.map((file) => + copy(join(sourceDir, distDir, file), join(destDir, distDir, file), { + recursive: true, + }), + ), + ); + + return { packageJson, frameworksEntry: "next.js", dotEnv }; +} + +/** + * Create a dev server. + */ +export async function getDevModeHandle(dir: string, _: string, hostingEmulatorInfo?: EmulatorInfo) { + // throw error when using Next.js middleware with firebase serve + if (!hostingEmulatorInfo) { + if (await isUsingMiddleware(dir, true)) { + throw new FirebaseError( + `${clc.bold("firebase serve")} does not support Next.js Middleware. Please use ${clc.bold( + "firebase emulators:start", + )} instead.`, + ); + } + } + + let next = await relativeRequire(dir, "next"); + if ("default" in next) next = next.default; + const nextApp = next({ + dev: true, + dir, + hostname: hostingEmulatorInfo?.host, + port: hostingEmulatorInfo?.port, + }); + const handler = nextApp.getRequestHandler(); + await nextApp.prepare(); + + return simpleProxy(async (req: IncomingMessage, res: ServerResponse) => { + const parsedUrl = parse(req.url!, true); + await handler(req, res, parsedUrl); + }); +} + +async function getConfig( + dir: string, +): Promise & { distDir: string; trailingSlash: boolean; basePath: string }> { + let config: NextConfig = {}; + const configFile = await whichNextConfigFile(dir); + if (configFile) { + const version = getNextVersion(dir); + if (!version) throw new Error("Unable to find the next dep, try NPM installing?"); + if (gte(version, "12.0.0")) { + const [{ default: loadConfig }, { PHASE_PRODUCTION_BUILD }] = await Promise.all([ + relativeRequire(dir, "next/dist/server/config"), + relativeRequire(dir, "next/constants"), + ]); + config = await loadConfig(PHASE_PRODUCTION_BUILD, dir); + } else { + try { + config = await import(pathToFileURL(join(dir, configFile)).toString()); + } catch (e) { + throw new Error(`Unable to load ${configFile}.`); + } + } + } + validateLocales(config.i18n?.locales); + return { + distDir: ".next", + // trailingSlash defaults to false in Next.js: https://nextjs.org/docs/api-reference/next.config.js/trailing-slash + trailingSlash: false, + basePath: "/", + ...config, + }; +} diff --git a/src/frameworks/next/interfaces.ts b/src/frameworks/next/interfaces.ts new file mode 100644 index 00000000000..00abdbc5c2e --- /dev/null +++ b/src/frameworks/next/interfaces.ts @@ -0,0 +1,161 @@ +import type { RouteHas } from "next/dist/lib/load-custom-routes"; +import type { ImageConfigComplete } from "next/dist/shared/lib/image-config"; +import type { MiddlewareManifest as MiddlewareManifestV2FromNext } from "next/dist/build/webpack/plugins/middleware-plugin"; +import type { HostingHeaders } from "../../firebaseConfig"; +import type { CONFIG_FILES } from "./constants"; + +export interface RoutesManifestRewriteObject { + beforeFiles?: RoutesManifestRewrite[]; + afterFiles?: RoutesManifestRewrite[]; + fallback?: RoutesManifestRewrite[]; +} + +export interface RoutesManifestRedirect { + source: string; + destination: string; + locale?: false; + internal?: boolean; + statusCode: number; + regex: string; + has?: RouteHas[]; + missing?: RouteHas[]; +} + +export interface RoutesManifestRewrite { + source: string; + destination: string; + has?: RouteHas[]; + missing?: RouteHas[]; + regex: string; +} + +export interface RoutesManifestHeader { + source: string; + headers: { key: string; value: string }[]; + has?: RouteHas[]; + missing?: RouteHas[]; + regex: string; +} + +// Next.js's exposed interface is incomplete here +export interface RoutesManifest { + version: number; + pages404: boolean; + basePath: string; + redirects: Array; + rewrites?: Array | RoutesManifestRewriteObject; + headers: Array; + staticRoutes: Array<{ + page: string; + regex: string; + namedRegex?: string; + routeKeys?: { [key: string]: string }; + }>; + dynamicRoutes: Array<{ + page: string; + regex: string; + namedRegex?: string; + routeKeys?: { [key: string]: string }; + }>; + dataRoutes: Array<{ + page: string; + routeKeys?: { [key: string]: string }; + dataRouteRegex: string; + namedDataRouteRegex?: string; + }>; + i18n?: { + domains?: Array<{ + http?: true; + domain: string; + locales?: string[]; + defaultLocale: string; + }>; + locales: string[]; + defaultLocale: string; + localeDetection?: false; + }; +} + +export interface ExportMarker { + version: number; + hasExportPathMap: boolean; + exportTrailingSlash: boolean; + isNextImageImported: boolean; +} + +export type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2FromNext; + +export type MiddlewareManifestV2 = MiddlewareManifestV2FromNext; + +// See: https://github.com/vercel/next.js/blob/b188fab3360855c28fd9407bd07c4ee9f5de16a6/packages/next/build/webpack/plugins/middleware-plugin.ts#L15-L29 +export interface MiddlewareManifestV1 { + version: 1; + sortedMiddleware: string[]; + clientInfo: [location: string, isSSR: boolean][]; + middleware: { + [page: string]: { + env: string[]; + files: string[]; + name: string; + page: string; + regexp: string; + wasm?: any[]; // WasmBinding isn't exported from next + }; + }; +} + +export interface ImagesManifest { + version: number; + images: ImageConfigComplete & { + sizes: number[]; + }; +} + +export interface NpmLsDepdendency { + version?: string; + resolved?: string; + dependencies?: { + [key: string]: NpmLsDepdendency; + }; +} + +export interface NpmLsReturn { + version: string; + name: string; + dependencies: { + [key: string]: NpmLsDepdendency; + }; +} + +export interface AppPathsManifest { + [key: string]: string; +} + +export interface HostingHeadersWithSource { + source: string; + headers: HostingHeaders["headers"]; +} + +export type AppPathRoutesManifest = Record; + +/** + * Note: This is a copy of the type from `next/dist/build/webpack/plugins/flight-client-entry-plugin`. + * It's copied here due to type errors caused by internal dependencies of Next.js when importing that file. + */ +export type ActionManifest = { + encryptionKey: string; + node: Actions; + edge: Actions; +}; +type Actions = { + [actionId: string]: { + workers: { + [name: string]: string | number; + }; + layer: { + [name: string]: string; + }; + }; +}; + +export type NextConfigFileName = (typeof CONFIG_FILES)[number]; diff --git a/src/frameworks/next/testing/app.ts b/src/frameworks/next/testing/app.ts new file mode 100644 index 00000000000..e4e77b63817 --- /dev/null +++ b/src/frameworks/next/testing/app.ts @@ -0,0 +1,92 @@ +import { PrerenderManifest } from "next/dist/build"; +import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin"; +import type { ActionManifest, AppPathRoutesManifest, AppPathsManifest } from "../interfaces"; + +export const appPathsManifest: AppPathsManifest = { + "/api/test/route": "app/api/test/route.js", + "/api/static/route": "app/api/static/route.js", + "/page": "app/page.js", +}; + +export const appPathRoutesManifest: AppPathRoutesManifest = { + "/api/test/route": "/api/test", + "/api/static/route": "/api/static", + "/page": "/", + "/another-s-a/page": "/another-s-a", + "/server-action/page": "/server-action", + "/ssr/page": "/ssr", + "/server-action/edge/page": "/server-action/edge", +}; + +export const pagesManifest: PagesManifest = { + "/_app": "pages/_app.js", + "/_document": "pages/_document.js", + "/_error": "pages/_error.js", + "/404": "pages/404.html", + "/dynamic/[dynamic-slug]": "pages/dynamic/[dynamic-slug].js", +}; + +export const prerenderManifest: PrerenderManifest = { + version: 4, + routes: { + "/": { + initialRevalidateSeconds: false, + srcRoute: "/", + dataRoute: "/index.rsc", + experimentalPPR: false, + prefetchDataRoute: "", + }, + "/api/static": { + initialRevalidateSeconds: false, + srcRoute: "/api/static", + dataRoute: "", + experimentalPPR: false, + prefetchDataRoute: "", + }, + }, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: { + previewModeId: "123", + previewModeSigningKey: "123", + previewModeEncryptionKey: "123", + }, +}; + +// content of a .meta file +export const metaFileContents = { + status: 200, + headers: { "content-type": "application/json", "custom-header": "custom-value" }, +} as const; + +export const pageClientReferenceManifestWithImage = `globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {}; +globalThis.__RSC_MANIFEST["/page"] = + '{"ssrModuleMapping":{"372":{"*":{"id":"772","name":"*","chunks":[],"async":false}},"1223":{"*":{"id":"4249","name":"*","chunks":[],"async":false}},"3240":{"*":{"id":"7230","name":"*","chunks":[],"async":false}},"3466":{"*":{"id":"885","name":"*","chunks":[],"async":false}},"5721":{"*":{"id":"8262","name":"*","chunks":[],"async":false}},"8095":{"*":{"id":"4564","name":"*","chunks":[],"async":false}}},"edgeSSRModuleMapping":{},"clientModules":{"/app-path/node_modules/next/dist/client/components/error-boundary.js":{"id":1223,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/error-boundary.js":{"id":1223,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/app-router.js":{"id":8095,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/app-router.js":{"id":8095,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/layout-router.js":{"id":3466,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/layout-router.js":{"id":3466,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/render-from-template-context.js":{"id":372,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/render-from-template-context.js":{"id":372,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/static-generation-searchparams-bailout-provider.js":{"id":5721,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/static-generation-searchparams-bailout-provider.js":{"id":5721,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/image-component.js":{"id":3240,"name":"*","chunks":["931:static/chunks/app/page-63aef8294f0aa02c.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/image-component.js":{"id":3240,"name":"*","chunks":["931:static/chunks/app/page-63aef8294f0aa02c.js"],"async":false},"/app-path/node_modules/next/font/google/target.css?{\\"path\\":\\"src/app/layout.tsx\\",\\"import\\":\\"Inter\\",\\"arguments\\":[{\\"subsets\\":[\\"latin\\"]}],\\"variableName\\":\\"inter\\"}":{"id":794,"name":"*","chunks":["185:static/chunks/app/layout-1a019a4780e5374b.js"],"async":false},"/app-path/src/app/globals.css":{"id":54,"name":"*","chunks":["185:static/chunks/app/layout-1a019a4780e5374b.js"],"async":false}},"entryCSSFiles":{"/app-path/src/app/page":[],"/app-path/src/app/layout":["static/css/decca5dbb1efb27a.css"]}}';`; + +export const pageClientReferenceManifestWithoutImage = `globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {}; +globalThis.__RSC_MANIFEST["/page"] = + '{"ssrModuleMapping":{"372":{"*":{"id":"772","name":"*","chunks":[],"async":false}},"1223":{"*":{"id":"4249","name":"*","chunks":[],"async":false}},"3240":{"*":{"id":"7230","name":"*","chunks":[],"async":false}},"3466":{"*":{"id":"885","name":"*","chunks":[],"async":false}},"5721":{"*":{"id":"8262","name":"*","chunks":[],"async":false}},"8095":{"*":{"id":"4564","name":"*","chunks":[],"async":false}}},"edgeSSRModuleMapping":{},"clientModules":{"/app-path/node_modules/next/dist/client/components/error-boundary.js":{"id":1223,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/error-boundary.js":{"id":1223,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/app-router.js":{"id":8095,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/app-router.js":{"id":8095,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/layout-router.js":{"id":3466,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/layout-router.js":{"id":3466,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/render-from-template-context.js":{"id":372,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/render-from-template-context.js":{"id":372,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/client/components/static-generation-searchparams-bailout-provider.js":{"id":5721,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/static-generation-searchparams-bailout-provider.js":{"id":5721,"name":"*","chunks":["272:static/chunks/webpack-524fad5a962db320.js","253:static/chunks/bce60fc1-3138fc63e84359d9.js","961:static/chunks/961-8f7137d989a0e4e3.js"],"async":false},"/app-path/node_modules/next/font/google/target.css?{\\"path\\":\\"src/app/layout.tsx\\",\\"import\\":\\"Inter\\",\\"arguments\\":[{\\"subsets\\":[\\"latin\\"]}],\\"variableName\\":\\"inter\\"}":{"id":794,"name":"*","chunks":["185:static/chunks/app/layout-1a019a4780e5374b.js"],"async":false},"/app-path/src/app/globals.css":{"id":54,"name":"*","chunks":["185:static/chunks/app/layout-1a019a4780e5374b.js"],"async":false}},"entryCSSFiles":{"/app-path/src/app/page":[],"/app-path/src/app/layout":["static/css/decca5dbb1efb27a.css"]}}';`; + +export const clientReferenceManifestWithImage = `{"ssrModuleMapping":{"2306":{"*":{"id":"7833","name":"*","chunks":[],"async":false}},"2353":{"*":{"id":"8709","name":"*","chunks":[],"async":false}},"3029":{"*":{"id":"9556","name":"*","chunks":[],"async":false}},"7330":{"*":{"id":"7734","name":"*","chunks":[],"async":false}},"8531":{"*":{"id":"9150","name":"*","chunks":[],"async":false}},"9180":{"*":{"id":"2698","name":"*","chunks":[],"async":false}}},"edgeSSRModuleMapping":{},"clientModules":{"/app-path/node_modules/next/dist/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/image-component.js":{"id":3029,"name":"*","chunks":["931:static/chunks/app/page-8d47763b987bba19.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/image-component.js":{"id":3029,"name":"*","chunks":["931:static/chunks/app/page-8d47763b987bba19.js"],"async":false},"/app-path/node_modules/next/font/google/target.css?{\"path\":\"src/app/layout.tsx\",\"import\":\"Inter\",\"arguments\":[{\"subsets\":[\"latin\"]}],\"variableName\":\"inter\"}":{"id":670,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false},"/app-path/src/app/globals.css":{"id":8410,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false}},"entryCSSFiles":{"/app-path/src/app/page":[],"/app-path/src/app/layout":["static/css/110a35ea7c81b899.css"]}}`; + +export const clientReferenceManifestWithoutImage = `{"ssrModuleMapping":{"2306":{"*":{"id":"7833","name":"*","chunks":[],"async":false}},"2353":{"*":{"id":"8709","name":"*","chunks":[],"async":false}},"3029":{"*":{"id":"9556","name":"*","chunks":[],"async":false}},"7330":{"*":{"id":"7734","name":"*","chunks":[],"async":false}},"8531":{"*":{"id":"9150","name":"*","chunks":[],"async":false}},"9180":{"*":{"id":"2698","name":"*","chunks":[],"async":false}}},"edgeSSRModuleMapping":{},"clientModules":{"/app-path/node_modules/next/dist/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/app-router.js":{"id":2353,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/layout-router.js":{"id":9180,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/render-from-template-context.js":{"id":2306,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/static-generation-searchparams-bailout-provider.js":{"id":8531,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/dist/esm/client/components/error-boundary.js":{"id":7330,"name":"*","chunks":["272:static/chunks/webpack-76fd8b39fe914c29.js","253:static/chunks/bce60fc1-8c4748991edb1ec4.js","698:static/chunks/698-1321e6d13d35448d.js"],"async":false},"/app-path/node_modules/next/font/google/target.css?{\"path\":\"src/app/layout.tsx\",\"import\":\"Inter\",\"arguments\":[{\"subsets\":[\"latin\"]}],\"variableName\":\"inter\"}":{"id":670,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false},"/app-path/src/app/globals.css":{"id":8410,"name":"*","chunks":["185:static/chunks/app/layout-09ef1f5c8b0e56d1.js"],"async":false}},"entryCSSFiles":{"/app-path/src/app/page":[],"/app-path/src/app/layout":["static/css/110a35ea7c81b899.css"]}}`; + +export const serverReferenceManifest: ActionManifest = { + node: { + "123": { + workers: { "app/another-s-a/page": 123, "app/server-action/page": 123 }, + layer: { + "app/another-s-a/page": "action-browser", + "app/server-action/page": "action-browser", + "app/ssr/page": "rsc", + }, + }, + }, + edge: { + "123": { + workers: { "app/server-action/edge/page": 123 }, + layer: { "app/server-action/edge/page": "action-browser" }, + }, + }, + encryptionKey: "456", +}; diff --git a/src/frameworks/next/testing/headers.ts b/src/frameworks/next/testing/headers.ts new file mode 100644 index 00000000000..e31e622395c --- /dev/null +++ b/src/frameworks/next/testing/headers.ts @@ -0,0 +1,297 @@ +import type { RoutesManifestHeader } from "../interfaces"; +import { supportedPaths, unsupportedPaths } from "./paths"; + +export const supportedHeaders: RoutesManifestHeader[] = [ + ...supportedPaths.map((path) => ({ + source: path, + regex: "", + headers: [], + })), + { + regex: "", + source: "/add-header", + headers: [ + { + key: "x-custom-header", + value: "hello world", + }, + { + key: "x-another-header", + value: "hello again", + }, + ], + }, + { + regex: "", + source: "/my-other-header/:path", + headers: [ + { + key: "x-path", + value: ":path", + }, + { + key: "some:path", + value: "hi", + }, + { + key: "x-test", + value: "some:value*", + }, + { + key: "x-test-2", + value: "value*", + }, + { + key: "x-test-3", + value: ":value?", + }, + { + key: "x-test-4", + value: ":value+", + }, + { + key: "x-test-5", + value: "something https:", + }, + { + key: "x-test-6", + value: ":hello(world)", + }, + { + key: "x-test-7", + value: "hello(world)", + }, + { + key: "x-test-8", + value: "hello{1,}", + }, + { + key: "x-test-9", + value: ":hello{1,2}", + }, + { + key: "content-security-policy", + value: + "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path", + }, + ], + }, + { + regex: "", + source: "/without-params/url", + headers: [ + { + key: "x-origin", + value: "https://example.com", + }, + ], + }, + { + regex: "", + source: "/with-params/url/:path*", + headers: [ + { + key: "x-url", + value: "https://example.com/:path*", + }, + ], + }, + { + regex: "", + source: "/with-params/url2/:path*", + headers: [ + { + key: "x-url", + value: "https://example.com:8080?hello=:path*", + }, + ], + }, + { + regex: "", + source: "/:path*", + headers: [ + { + key: "x-something", + value: "applied-everywhere", + }, + ], + }, + { + regex: "", + source: "/catchall-header/:path*", + headers: [ + { + key: "x-value", + value: ":path*", + }, + ], + }, + { + regex: "", + source: "/named-pattern/:path(.*)", + headers: [ + { + key: "x-something", + value: "value=:path", + }, + { + key: "path-:path", + value: "end", + }, + ], + }, + { + regex: "", + source: "/my-headers/(.*)", + headers: [ + { + key: "x-first-header", + value: "first", + }, + { + key: "x-second-header", + value: "second", + }, + ], + }, +]; + +export const unsupportedHeaders: RoutesManifestHeader[] = [ + ...unsupportedPaths.map((path) => ({ + source: path, + regex: "", + headers: [], + })), + { + regex: "", + source: "/has-header-1", + has: [ + { + type: "header", + key: "x-my-header", + value: "(?.*)", + }, + ], + headers: [ + { + key: "x-another", + value: "header", + }, + ], + }, + { + regex: "", + source: "/has-header-2", + has: [ + { + type: "query", + key: "my-query", + }, + ], + headers: [ + { + key: "x-added", + value: "value", + }, + ], + }, + { + regex: "", + source: "/has-header-3", + has: [ + { + type: "cookie", + key: "loggedIn", + value: "true", + }, + ], + headers: [ + { + key: "x-is-user", + value: "yuuuup", + }, + ], + }, + { + regex: "", + source: "/has-header-4", + has: [ + { + type: "host", + value: "example.com", + }, + ], + headers: [ + { + key: "x-is-host", + value: "yuuuup", + }, + ], + }, + { + regex: "", + source: "/missing-header-1", + missing: [ + { + type: "header", + key: "x-my-header", + value: "(?.*)", + }, + ], + headers: [ + { + key: "x-another", + value: "header", + }, + ], + }, + { + regex: "", + source: "/missing-header-2", + missing: [ + { + type: "query", + key: "my-query", + }, + ], + headers: [ + { + key: "x-added", + value: "value", + }, + ], + }, + { + regex: "", + source: "/missing-header-3", + missing: [ + { + type: "cookie", + key: "loggedIn", + value: "true", + }, + ], + headers: [ + { + key: "x-is-user", + value: "yuuuup", + }, + ], + }, + { + regex: "", + source: "/missing-header-4", + missing: [ + { + type: "host", + value: "example.com", + }, + ], + headers: [ + { + key: "x-is-host", + value: "yuuuup", + }, + ], + }, +]; diff --git a/src/frameworks/next/testing/i18n.ts b/src/frameworks/next/testing/i18n.ts new file mode 100644 index 00000000000..29db774b996 --- /dev/null +++ b/src/frameworks/next/testing/i18n.ts @@ -0,0 +1,50 @@ +import type { DomainLocale } from "next/dist/server/config"; + +export const pathsWithCustomRoutesInternalPrefix = [ + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/bar/:slug(\\d{1,})`, + `/:nextInternalLocale/bar/:slug`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/bar/bar`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/bar/\\(escapedparentheses\\)/:slug(\\d{1,})`, + `/:nextInternalLocale/bar/barbar`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/bar/another-regex/((?!bar).*)`, + `/:nextInternalLocale/bar/barbar`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/team`, + `/:nextInternalLocale/bar/barbar`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/about-us`, + `/:nextInternalLocale/bar/barbar`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/post/:slug`, + `/:nextInternalLocale/bar/barbar`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/blog/:slug*`, + `/:nextInternalLocale/bar/barbar`, + `/:nextInternalLocale(en\\-US|fr|nl\\-NL|nl\\-BE)/docs/:slug`, + `/:nextInternalLocale/bar/barbar`, +]; + +export const i18nDomains: DomainLocale[] = [ + { + defaultLocale: "en-US", + domain: "en-us.firebaseapp.com", + }, + { + defaultLocale: "pt-BR", + domain: "pt-br.firebaseapp.com", + }, + { + defaultLocale: "es-ES", + domain: "es-es.firebaseapp.com", + }, + { + defaultLocale: "fr-FR", + domain: "fr-fr.firebaseapp.com", + }, + { + defaultLocale: "it-IT", + domain: "it-it.firebaseapp.com", + }, + { + defaultLocale: "de-DE", + domain: "de-de.firebaseapp.com", + }, +]; + +export const domains = i18nDomains.map(({ domain }) => domain); diff --git a/src/frameworks/next/testing/images.ts b/src/frameworks/next/testing/images.ts new file mode 100644 index 00000000000..c657396dc20 --- /dev/null +++ b/src/frameworks/next/testing/images.ts @@ -0,0 +1,51 @@ +import type { ExportMarker, ImagesManifest } from "../interfaces"; + +export const exportMarkerWithoutImage: ExportMarker = { + version: 1, + hasExportPathMap: false, + exportTrailingSlash: false, + isNextImageImported: false, +}; + +export const exportMarkerWithImage: ExportMarker = { + version: 1, + hasExportPathMap: false, + exportTrailingSlash: false, + isNextImageImported: true, +}; + +export const imagesManifest: ImagesManifest = { + version: 1, + images: { + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + path: "/_next/image", + loader: "default", + loaderFile: "", + domains: [], + disableStaticImages: false, + minimumCacheTTL: 60, + formats: ["image/avif", "image/webp"], + dangerouslyAllowSVG: false, + contentSecurityPolicy: "script-src 'none'; frame-src 'none'; sandbox;", + contentDispositionType: "inline", + remotePatterns: [ + { + protocol: "https", + hostname: "^(?:^(?:assets\\.vercel\\.com)$)$", + port: "", + pathname: "^(?:\\/image\\/upload(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)|$))$", + }, + ], + unoptimized: false, + sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840, 16, 32, 48, 64, 96, 128, 256, 384], + }, +}; + +export const imagesManifestUnoptimized: ImagesManifest = { + ...imagesManifest, + images: { + ...imagesManifest.images, + unoptimized: true, + }, +}; diff --git a/src/frameworks/next/testing/index.ts b/src/frameworks/next/testing/index.ts new file mode 100644 index 00000000000..a50d4fca72d --- /dev/null +++ b/src/frameworks/next/testing/index.ts @@ -0,0 +1,8 @@ +export * from "./paths"; +export * from "./headers"; +export * from "./redirects"; +export * from "./rewrites"; +export * from "./images"; +export * from "./middleware"; +export * from "./npm"; +export * from "./app"; diff --git a/src/frameworks/next/testing/middleware.ts b/src/frameworks/next/testing/middleware.ts new file mode 100644 index 00000000000..69750060d24 --- /dev/null +++ b/src/frameworks/next/testing/middleware.ts @@ -0,0 +1,53 @@ +import type { MiddlewareManifestV1, MiddlewareManifestV2 } from "../interfaces"; + +export const middlewareV2ManifestWhenUsed: MiddlewareManifestV2 = { + sortedMiddleware: ["/"], + middleware: { + "/": { + files: ["server/edge-runtime-webpack.js", "server/middleware.js"], + name: "middleware", + page: "/", + matchers: [ + { + regexp: + "^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))\\/about(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?(.json)?[\\/#\\?]?$", + originalSource: "", + }, + ], + wasm: [], + assets: [], + }, + }, + functions: {}, + version: 2, +}; + +export const middlewareV2ManifestWhenNotUsed: MiddlewareManifestV2 = { + sortedMiddleware: [], + middleware: {}, + functions: {}, + version: 2, +}; + +export const middlewareV1ManifestWhenUsed: MiddlewareManifestV1 = { + sortedMiddleware: ["/"], + clientInfo: [["/", false]], + middleware: { + "/": { + env: [], + files: ["server/edge-runtime-webpack.js", "server/pages/_middleware.js"], + name: "pages/_middleware", + page: "/", + regexp: "^/(?!_next).*$", + wasm: [], + }, + }, + version: 1, +}; + +export const middlewareV1ManifestWhenNotUsed: MiddlewareManifestV1 = { + sortedMiddleware: [], + clientInfo: [], + middleware: {}, + version: 1, +}; diff --git a/src/frameworks/next/testing/npm.ts b/src/frameworks/next/testing/npm.ts new file mode 100644 index 00000000000..4dd136147a7 --- /dev/null +++ b/src/frameworks/next/testing/npm.ts @@ -0,0 +1,129 @@ +import { NpmLsReturn } from "../interfaces"; + +export const npmLsReturn: NpmLsReturn = { + version: "0.1.0", + name: "next-next", + dependencies: { + "@next/font": { + version: "13.0.6", + resolved: "https://registry.npmjs.org/@next/font/-/font-13.0.6.tgz", + }, + next: { + version: "13.0.6", + resolved: "https://registry.npmjs.org/next/-/next-13.0.6.tgz", + dependencies: { + "@next/env": { + version: "13.0.6", + resolved: "https://registry.npmjs.org/@next/env/-/env-13.0.6.tgz", + }, + "@next/swc-android-arm-eabi": {}, + "@next/swc-android-arm64": {}, + "@next/swc-darwin-arm64": {}, + "@next/swc-darwin-x64": { + version: "13.0.6", + resolved: "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.6.tgz", + }, + "@next/swc-freebsd-x64": {}, + "@next/swc-linux-arm-gnueabihf": {}, + "@next/swc-linux-arm64-gnu": {}, + "@next/swc-linux-arm64-musl": {}, + "@next/swc-linux-x64-gnu": {}, + "@next/swc-linux-x64-musl": {}, + "@next/swc-win32-arm64-msvc": {}, + "@next/swc-win32-ia32-msvc": {}, + "@next/swc-win32-x64-msvc": {}, + "@swc/helpers": { + version: "0.4.14", + resolved: "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", + dependencies: { + tslib: { + version: "2.4.1", + resolved: "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + }, + }, + }, + "caniuse-lite": { + version: "1.0.30001439", + resolved: "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz", + }, + fibers: {}, + "node-sass": {}, + postcss: { + version: "8.4.14", + resolved: "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + dependencies: { + nanoid: { + version: "3.3.4", + resolved: "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + }, + picocolors: { + version: "1.0.0", + resolved: "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + }, + "source-map-js": { + version: "1.0.2", + resolved: "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + }, + }, + }, + "react-dom": { + version: "18.2.0", + }, + react: { + version: "18.2.0", + }, + sass: {}, + "styled-jsx": { + version: "5.1.0", + resolved: "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.0.tgz", + dependencies: { + "client-only": { + version: "0.0.1", + resolved: "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + }, + react: { + version: "18.2.0", + }, + }, + }, + }, + }, + "react-dom": { + version: "18.2.0", + resolved: "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + dependencies: { + "loose-envify": { + version: "1.4.0", + resolved: "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + dependencies: { + "js-tokens": { + version: "4.0.0", + resolved: "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + }, + }, + }, + react: { + version: "18.2.0", + }, + scheduler: { + version: "0.23.0", + resolved: "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + dependencies: { + "loose-envify": { + version: "1.4.0", + }, + }, + }, + }, + }, + react: { + version: "18.2.0", + resolved: "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + dependencies: { + "loose-envify": { + version: "1.4.0", + }, + }, + }, + }, +}; diff --git a/src/frameworks/next/testing/paths.ts b/src/frameworks/next/testing/paths.ts new file mode 100644 index 00000000000..e11d316f9a1 --- /dev/null +++ b/src/frameworks/next/testing/paths.ts @@ -0,0 +1,97 @@ +export const pathsWithRegex = [ + "/(.*)", + "/post/:slug(\\d{1,})", + "/api-hello-regex/:first(.*)", + "/unnamed-params/nested/(.*)/:test/(.*)", + "/:path((?!another-page$).*)", +] as const; + +export const pathsWithEscapedChars = [ + `/post\\(someStringBetweenParentheses\\)/:slug`, + `/english\\(default\\)/:slug`, +] as const; + +export const pathsWithRegexAndEscapedChars = [ + `/post/\\(escapedparentheses\\)/:slug(\\d{1,})`, + `/post/\\(es\\?cap\\Wed\\*p\\{ar\\}en\\:th\\eses\\)`, + `/post/\\(es\\?cap\\Wed\\*p\\{ar\\}en\\:th\\eses\\)/:slug(\\d{1,})`, +] as const; + +export const pathsAsGlobs = [ + "/specific/:path*", + "/another/:path*", + "/about", + "/", + "/old-blog/:path*", + "/blog/:path*", + "/to-websocket", + "/to-nowhere", + "/rewriting-to-auto-export", + "/rewriting-to-another-auto-export/:path*", + "/to-another", + "/another/one", + "/nav", + "/404", + "/hello-world", + "/static/hello.txt", + "/another", + "/multi-rewrites", + "/first", + "/hello", + "/second", + "/hello-again", + "/to-hello", + "/hello", + "/blog/post-1", + "/blog/post-2", + "/test/:path", + "/:path", + "/test-overwrite/:something/:another", + "/params/this-should-be-the-value", + "/params/:something", + "/with-params", + "/query-rewrite/:section/:name", + "/hidden/_next/:path*", + "/_next/:path*", + "/proxy-me/:path*", + "/api-hello", + "/api/hello", + "/api-hello-param/:name", + "/api-dynamic-param/:name", + "/api/hello?name=:first*", + "/api/hello?hello=:name", + "/api/dynamic/:name?hello=:name", + "/:path/post-321", + "/with-params", + "/with-params", + "/catchall-rewrite/:path*", + "/with-params", + "/catchall-query/:path*", + "/has-rewrite-1", + "/has-rewrite-2", + "/has-rewrite-3", + "/has-rewrite-4", + "/has-rewrite-5", + "/:hasParam", + "/has-rewrite-6", + "/with-params", + "/has-rewrite-7", + "/has-rewrite-8", + "/blog-catchall/:post", + "/missing-rewrite-1", + "/with-params", + "/missing-rewrite-2", + "/with-params", + "/missing-rewrite-3", + "/overridden/:path*", +] as const; + +export const supportedPaths = [ + ...pathsWithEscapedChars, + ...pathsAsGlobs, + ...pathsWithRegex, + ...pathsWithRegexAndEscapedChars, +] as const; + +// It seems as though we support all these! +export const unsupportedPaths = [] as const; diff --git a/src/frameworks/next/testing/redirects.ts b/src/frameworks/next/testing/redirects.ts new file mode 100644 index 00000000000..521d0904f71 --- /dev/null +++ b/src/frameworks/next/testing/redirects.ts @@ -0,0 +1,199 @@ +import type { RoutesManifestRedirect } from "../interfaces"; +import { supportedPaths, unsupportedPaths } from "./paths"; + +export const supportedRedirects: RoutesManifestRedirect[] = supportedPaths.map((path) => ({ + source: path, + destination: `/redirect`, + regex: "", + statusCode: 301, +})); + +export const unsupportedRedirects: RoutesManifestRedirect[] = [ + ...unsupportedPaths.map((path) => ({ + source: path, + destination: `/redirect`, + regex: "", + statusCode: 301, + })), + { + source: "/has-redirect-1", + has: [ + { + type: "header", + key: "x-my-header", + value: "(?.*)", + }, + ], + destination: "/another?myHeader=:myHeader", + statusCode: 307, + regex: "", + }, + { + source: "/has-redirect-2", + has: [ + { + type: "query", + key: "my-query", + }, + ], + destination: "/another?value=:myquery", + statusCode: 307, + regex: "", + }, + { + source: "/has-redirect-3", + has: [ + { + type: "cookie", + key: "loggedIn", + value: "true", + }, + ], + destination: "/another?authorized=1", + statusCode: 307, + regex: "", + }, + { + source: "/has-redirect-4", + has: [ + { + type: "host", + value: "example.com", + }, + ], + destination: "/another?host=1", + statusCode: 307, + regex: "", + }, + { + source: "/:path/has-redirect-5", + has: [ + { + type: "header", + key: "x-test-next", + }, + ], + destination: "/somewhere", + statusCode: 307, + regex: "", + }, + { + source: "/has-redirect-6", + has: [ + { + type: "host", + value: "(?.*)-test.example.com", + }, + ], + destination: "https://:subdomain.example.com/some-path/end?a=b", + statusCode: 307, + regex: "", + }, + { + source: "/has-redirect-7", + has: [ + { + type: "query", + key: "hello", + value: "(?.*)", + }, + ], + destination: "/somewhere?value=:hello", + statusCode: 307, + regex: "", + }, + { + source: "/internal-redirect-1", + internal: true, + destination: "/somewhere?value=:hello", + statusCode: 307, + regex: "", + }, + { + source: "/missing-redirect-1", + missing: [ + { + type: "header", + key: "x-my-header", + value: "(?.*)", + }, + ], + destination: "/another?myHeader=:myHeader", + statusCode: 307, + regex: "", + }, + { + source: "/missing-redirect-2", + missing: [ + { + type: "query", + key: "my-query", + }, + ], + destination: "/another?value=:myquery", + statusCode: 307, + regex: "", + }, + { + source: "/missing-redirect-3", + missing: [ + { + type: "cookie", + key: "loggedIn", + value: "true", + }, + ], + destination: "/another?authorized=1", + statusCode: 307, + regex: "", + }, + { + source: "/missing-redirect-4", + missing: [ + { + type: "host", + value: "example.com", + }, + ], + destination: "/another?host=1", + statusCode: 307, + regex: "", + }, + { + source: "/:path/missing-redirect-5", + missing: [ + { + type: "header", + key: "x-test-next", + }, + ], + destination: "/somewhere", + statusCode: 307, + regex: "", + }, + { + source: "/missing-redirect-6", + missing: [ + { + type: "host", + value: "(?.*)-test.example.com", + }, + ], + destination: "https://:subdomain.example.com/some-path/end?a=b", + statusCode: 307, + regex: "", + }, + { + source: "/missing-redirect-7", + missing: [ + { + type: "query", + key: "hello", + value: "(?.*)", + }, + ], + destination: "/somewhere?value=:hello", + statusCode: 307, + regex: "", + }, +]; diff --git a/src/frameworks/next/testing/rewrites.ts b/src/frameworks/next/testing/rewrites.ts new file mode 100644 index 00000000000..d2d92fad5df --- /dev/null +++ b/src/frameworks/next/testing/rewrites.ts @@ -0,0 +1,129 @@ +import type { RoutesManifestRewrite, RoutesManifestRewriteObject } from "../interfaces"; +import { supportedPaths, unsupportedPaths } from "./paths"; + +export const supportedRewritesArray: RoutesManifestRewrite[] = supportedPaths.map((path) => ({ + source: path, + destination: `/rewrite`, + regex: "", +})); + +export const unsupportedRewritesArray: RoutesManifestRewrite[] = [ + ...unsupportedPaths.map((path) => ({ + source: path, + destination: `/rewrite`, + regex: "", + })), + ...supportedPaths.map((path) => ({ + source: path, + destination: `/rewrite?arg=foo`, + regex: "", + })), + // external http URL + { + source: "/:path*", + destination: "http://firebase.google.com", + regex: "", + }, + // external https URL + { + source: "/:path*", + destination: "https://firebase.google.com", + regex: "", + }, + // with has + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + has: [ + { type: "query", key: "overrideMe" }, + { + type: "header", + key: "x-rewrite-me", + }, + ], + }, + // with has + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + has: [ + { + type: "query", + key: "page", + // the page value will not be available in the + // destination since value is provided and doesn't + // use a named capture group e.g. (?home) + value: "home", + }, + ], + }, + // with has + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + has: [ + { + type: "cookie", + key: "authorized", + value: "true", + }, + ], + }, + // with missing + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + missing: [ + { type: "query", key: "overrideMe" }, + { + type: "header", + key: "x-rewrite-me", + }, + ], + }, + // with missing + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + missing: [ + { + type: "query", + key: "page", + // the page value will not be available in the + // destination since value is provided and doesn't + // use a named capture group e.g. (?home) + value: "home", + }, + ], + }, + // with missing + { + source: "/specific/:path*", + destination: "/some/specific/:path", + regex: "", + missing: [ + { + type: "cookie", + key: "authorized", + value: "true", + }, + ], + }, +]; + +export const supportedRewritesObject: RoutesManifestRewriteObject = { + afterFiles: unsupportedRewritesArray, // should be ignored, only beforeFiles is used + beforeFiles: supportedRewritesArray, + fallback: unsupportedRewritesArray, // should be ignored, only beforeFiles is used +}; + +export const unsupportedRewritesObject: RoutesManifestRewriteObject = { + afterFiles: unsupportedRewritesArray, // should be ignored, only beforeFiles is used + beforeFiles: unsupportedRewritesArray, + fallback: unsupportedRewritesArray, // should be ignored, only beforeFiles is used +}; diff --git a/src/frameworks/next/utils.spec.ts b/src/frameworks/next/utils.spec.ts new file mode 100644 index 00000000000..7c30d7b0efc --- /dev/null +++ b/src/frameworks/next/utils.spec.ts @@ -0,0 +1,532 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as fsPromises from "fs/promises"; +import * as fsExtra from "fs-extra"; +import * as sinon from "sinon"; +import * as glob from "glob"; + +import { EXPORT_MARKER, IMAGES_MANIFEST, APP_PATH_ROUTES_MANIFEST } from "./constants"; + +import { + cleanEscapedChars, + isRewriteSupportedByHosting, + isRedirectSupportedByHosting, + isHeaderSupportedByHosting, + getNextjsRewritesToUse, + usesAppDirRouter, + usesNextImage, + hasUnoptimizedImage, + isUsingMiddleware, + isUsingImageOptimization, + isUsingAppDirectory, + cleanCustomRouteI18n, + I18N_SOURCE, + allDependencyNames, + getMiddlewareMatcherRegexes, + getNonStaticRoutes, + getNonStaticServerComponents, + getHeadersFromMetaFiles, + isUsingNextImageInAppDirectory, + getNextVersion, + getRoutesWithServerAction, +} from "./utils"; + +import * as frameworksUtils from "../utils"; +import * as fsUtils from "../../fsutils"; + +import { + exportMarkerWithImage, + exportMarkerWithoutImage, + imagesManifest, + imagesManifestUnoptimized, + middlewareV2ManifestWhenNotUsed, + middlewareV2ManifestWhenUsed, + supportedHeaders, + supportedRedirects, + supportedRewritesArray, + supportedRewritesObject, + unsupportedHeaders, + unsupportedRedirects, + unsupportedRewritesArray, + npmLsReturn, + middlewareV1ManifestWhenUsed, + middlewareV1ManifestWhenNotUsed, + pagesManifest, + prerenderManifest, + appPathsManifest, + appPathRoutesManifest, + metaFileContents, + pageClientReferenceManifestWithImage, + pageClientReferenceManifestWithoutImage, + clientReferenceManifestWithImage, + clientReferenceManifestWithoutImage, + serverReferenceManifest, +} from "./testing"; +import { pathsWithCustomRoutesInternalPrefix } from "./testing/i18n"; + +describe("Next.js utils", () => { + describe("cleanEscapedChars", () => { + it("should clean escaped chars", () => { + // path containing all escaped chars + const testPath = "/\\(\\)\\{\\}\\:\\+\\?\\*/:slug"; + + expect(testPath.includes("\\(")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\(")).to.be.false; + + expect(testPath.includes("\\)")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\)")).to.be.false; + + expect(testPath.includes("\\{")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\{")).to.be.false; + + expect(testPath.includes("\\}")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\}")).to.be.false; + + expect(testPath.includes("\\:")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\:")).to.be.false; + + expect(testPath.includes("\\+")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\+")).to.be.false; + + expect(testPath.includes("\\?")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\?")).to.be.false; + + expect(testPath.includes("\\*")).to.be.true; + expect(cleanEscapedChars(testPath).includes("\\*")).to.be.false; + }); + }); + + it("should allow supported rewrites", () => { + expect( + [...supportedRewritesArray, ...unsupportedRewritesArray].filter((it) => + isRewriteSupportedByHosting(it), + ), + ).to.have.members(supportedRewritesArray); + }); + + describe("isRedirectSupportedByFirebase", () => { + it("should allow supported redirects", () => { + expect( + [...supportedRedirects, ...unsupportedRedirects].filter((it) => + isRedirectSupportedByHosting(it), + ), + ).to.have.members(supportedRedirects); + }); + }); + + describe("isHeaderSupportedByFirebase", () => { + it("should allow supported headers", () => { + expect( + [...supportedHeaders, ...unsupportedHeaders].filter((it) => isHeaderSupportedByHosting(it)), + ).to.have.members(supportedHeaders); + }); + }); + + describe("getNextjsRewritesToUse", () => { + it("should use only beforeFiles", () => { + if (!supportedRewritesObject?.beforeFiles?.length) { + throw new Error("beforeFiles must have rewrites"); + } + + const rewritesToUse = getNextjsRewritesToUse(supportedRewritesObject); + + for (const [i, rewrite] of supportedRewritesObject.beforeFiles.entries()) { + expect(rewrite.source).to.equal(rewritesToUse[i].source); + expect(rewrite.destination).to.equal(rewritesToUse[i].destination); + } + }); + + it("should return all rewrites if in array format", () => { + const rewritesToUse = getNextjsRewritesToUse(supportedRewritesArray); + + expect(rewritesToUse).to.have.length(supportedRewritesArray.length); + }); + }); + + describe("usesAppDirRouter", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return false when app dir doesn't exist", () => { + sandbox.stub(fs, "existsSync").returns(false); + expect(usesAppDirRouter("")).to.be.false; + }); + + it("should return true when app dir does exist", () => { + sandbox.stub(fs, "existsSync").returns(true); + expect(usesAppDirRouter("")).to.be.true; + }); + }); + + describe("usesNextImage", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return true when export marker has isNextImageImported", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + isNextImageImported: true, + }); + expect(await usesNextImage("", "")).to.be.true; + }); + + it("should return false when export marker has !isNextImageImported", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + isNextImageImported: false, + }); + expect(await usesNextImage("", "")).to.be.false; + }); + }); + + describe("hasUnoptimizedImage", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return true when images manfiest indicates unoptimized", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + images: { unoptimized: true }, + }); + expect(await hasUnoptimizedImage("", "")).to.be.true; + }); + + it("should return true when images manfiest indicates !unoptimized", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + images: { unoptimized: false }, + }); + expect(await hasUnoptimizedImage("", "")).to.be.false; + }); + }); + + describe("isUsingMiddleware", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it("should return true if using middleware in development", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(true); + expect(await isUsingMiddleware("", true)).to.be.true; + }); + + it("should return false if not using middleware in development", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(false); + expect(await isUsingMiddleware("", true)).to.be.false; + }); + + it("should return true if using middleware in production", async () => { + sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenUsed); + expect(await isUsingMiddleware("", false)).to.be.true; + }); + + it("should return false if not using middleware in production", async () => { + sandbox.stub(fsExtra, "readJSON").resolves(middlewareV2ManifestWhenNotUsed); + expect(await isUsingMiddleware("", false)).to.be.false; + }); + }); + + describe("isUsingImageOptimization", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it("should return true if images optimization is used", async () => { + const stub = sandbox.stub(frameworksUtils, "readJSON"); + stub.withArgs(EXPORT_MARKER).resolves(exportMarkerWithImage); + stub.withArgs(IMAGES_MANIFEST).resolves(imagesManifest); + + expect(await isUsingImageOptimization("", "")).to.be.true; + }); + + it("should return false if isNextImageImported is false", async () => { + const stub = sandbox.stub(frameworksUtils, "readJSON"); + stub.withArgs(EXPORT_MARKER).resolves(exportMarkerWithoutImage); + + expect(await isUsingImageOptimization("", "")).to.be.false; + }); + + it("should return false if `unoptimized` option is used", async () => { + const stub = sandbox.stub(frameworksUtils, "readJSON"); + stub.withArgs(EXPORT_MARKER).resolves(exportMarkerWithImage); + stub.withArgs(IMAGES_MANIFEST).resolves(imagesManifestUnoptimized); + + expect(await isUsingImageOptimization("", "")).to.be.false; + }); + }); + + describe("isUsingNextImageInAppDirectory", () => { + describe("Next.js >= 13.4.10", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it("should return true when using next/image in the app directory", async () => { + sandbox + .stub(glob, "sync") + .returns(["/path-to-app/.next/server/app/page_client-reference-manifest.js"]); + sandbox.stub(fsPromises, "readFile").resolves(pageClientReferenceManifestWithImage); + + expect(await isUsingNextImageInAppDirectory("", "")).to.be.true; + }); + + it("should return false when not using next/image in the app directory", async () => { + sandbox.stub(fsPromises, "readFile").resolves(pageClientReferenceManifestWithoutImage); + const globStub = sandbox + .stub(glob, "sync") + .returns(["/path-to-app/.next/server/app/page_client-reference-manifest.js"]); + + expect(await isUsingNextImageInAppDirectory("", "")).to.be.false; + + globStub.restore(); + sandbox.stub(glob, "sync").returns([]); + + expect(await isUsingNextImageInAppDirectory("", "")).to.be.false; + }); + }); + + describe("Next.js < 13.4.10", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it("should return true when using next/image in the app directory", async () => { + sandbox.stub(fsPromises, "readFile").resolves(clientReferenceManifestWithImage); + sandbox + .stub(glob, "sync") + .returns(["/path-to-app/.next/server/client-reference-manifest.js"]); + + expect(await isUsingNextImageInAppDirectory("", "")).to.be.true; + }); + + it("should return false when not using next/image in the app directory", async () => { + sandbox.stub(fsPromises, "readFile").resolves(clientReferenceManifestWithoutImage); + sandbox.stub(glob, "sync").returns([]); + + expect(await isUsingNextImageInAppDirectory("", "")).to.be.false; + }); + }); + }); + + describe("isUsingAppDirectory", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it(`should return true if ${APP_PATH_ROUTES_MANIFEST} exists`, () => { + sandbox.stub(fsUtils, "fileExistsSync").returns(true); + + expect(isUsingAppDirectory("")).to.be.true; + }); + + it(`should return false if ${APP_PATH_ROUTES_MANIFEST} did not exist`, () => { + sandbox.stub(fsUtils, "fileExistsSync").returns(false); + + expect(isUsingAppDirectory("")).to.be.false; + }); + }); + + describe("cleanCustomRouteI18n", () => { + it("should remove Next.js i18n prefix", () => { + for (const path of pathsWithCustomRoutesInternalPrefix) { + const cleanPath = cleanCustomRouteI18n(path); + + expect(!!path.match(I18N_SOURCE)).to.be.true; + expect(!!cleanPath.match(I18N_SOURCE)).to.be.false; + + // should not keep double slashes + expect(cleanPath.startsWith("//")).to.be.false; + } + }); + }); + + describe("allDependencyNames", () => { + it("should return empty on stopping conditions", () => { + expect(allDependencyNames({})).to.eql([]); + expect(allDependencyNames({ version: "foo" })).to.eql([]); + }); + + it("should return expected dependency names", () => { + expect(allDependencyNames(npmLsReturn)).to.eql([ + "@next/font", + "next", + "@next/env", + "@next/swc-android-arm-eabi", + "@next/swc-android-arm64", + "@next/swc-darwin-arm64", + "@next/swc-darwin-x64", + "@next/swc-freebsd-x64", + "@next/swc-linux-arm-gnueabihf", + "@next/swc-linux-arm64-gnu", + "@next/swc-linux-arm64-musl", + "@next/swc-linux-x64-gnu", + "@next/swc-linux-x64-musl", + "@next/swc-win32-arm64-msvc", + "@next/swc-win32-ia32-msvc", + "@next/swc-win32-x64-msvc", + "@swc/helpers", + "tslib", + "caniuse-lite", + "fibers", + "node-sass", + "postcss", + "nanoid", + "picocolors", + "source-map-js", + "react-dom", + "react", + "sass", + "styled-jsx", + "client-only", + "react", + "react-dom", + "loose-envify", + "js-tokens", + "react", + "scheduler", + "loose-envify", + "react", + "loose-envify", + ]); + }); + }); + + describe("getMiddlewareMatcherRegexes", () => { + it("should return regexes when using version 1", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV1ManifestWhenUsed); + + for (const regex of middlewareMatcherRegexes) { + expect(regex).to.be.an.instanceOf(RegExp); + } + }); + + it("should return empty array when using version 1 but not using middleware", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV1ManifestWhenNotUsed); + + expect(middlewareMatcherRegexes).to.eql([]); + }); + + it("should return regexes when using version 2", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV2ManifestWhenUsed); + + for (const regex of middlewareMatcherRegexes) { + expect(regex).to.be.an.instanceOf(RegExp); + } + }); + + it("should return empty array when using version 2 but not using middleware", () => { + const middlewareMatcherRegexes = getMiddlewareMatcherRegexes(middlewareV2ManifestWhenNotUsed); + + expect(middlewareMatcherRegexes).to.eql([]); + }); + }); + + describe("getNonStaticRoutes", () => { + it("should get non-static routes", () => { + expect( + getNonStaticRoutes( + pagesManifest, + Object.keys(prerenderManifest.routes), + Object.keys(prerenderManifest.dynamicRoutes), + ), + ).to.deep.equal(["/dynamic/[dynamic-slug]"]); + }); + }); + + describe("getNonStaticServerComponents", () => { + it("should get non-static server components", () => { + expect( + getNonStaticServerComponents( + appPathsManifest, + appPathRoutesManifest, + Object.keys(prerenderManifest.routes), + Object.keys(prerenderManifest.dynamicRoutes), + ), + ).to.deep.equal(new Set(["/api/test/route"])); + }); + }); + + describe("getHeadersFromMetaFiles", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it("should get headers from meta files", async () => { + const distDir = ".next"; + const readJsonStub = sandbox.stub(frameworksUtils, "readJSON"); + const dirExistsSyncStub = sandbox.stub(fsUtils, "dirExistsSync"); + const fileExistsSyncStub = sandbox.stub(fsUtils, "fileExistsSync"); + + dirExistsSyncStub.withArgs(`${distDir}/server/app/api/static`).returns(true); + fileExistsSyncStub.withArgs(`${distDir}/server/app/api/static.meta`).returns(true); + readJsonStub.withArgs(`${distDir}/server/app/api/static.meta`).resolves(metaFileContents); + + expect( + await getHeadersFromMetaFiles(".", distDir, "/asdf", appPathRoutesManifest), + ).to.deep.equal([ + { + source: "/asdf/api/static", + headers: [ + { + key: "content-type", + value: "application/json", + }, + { + key: "custom-header", + value: "custom-value", + }, + ], + }, + ]); + }); + }); + + describe("getNextVersion", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it("should get version", () => { + sandbox.stub(frameworksUtils, "findDependency").returns({ version: "13.4.10" }); + + expect(getNextVersion("")).to.equal("13.4.10"); + }); + + it("should ignore canary version", () => { + sandbox.stub(frameworksUtils, "findDependency").returns({ version: "13.4.10-canary.0" }); + + expect(getNextVersion("")).to.equal("13.4.10"); + }); + + it("should return undefined if unable to get version", () => { + sandbox.stub(frameworksUtils, "findDependency").returns(undefined); + + expect(getNextVersion("")).to.be.undefined; + }); + }); + + describe("getRoutesWithServerAction", () => { + it("should get routes with server action", () => { + expect( + getRoutesWithServerAction(serverReferenceManifest, appPathRoutesManifest), + ).to.deep.equal(["/another-s-a", "/server-action", "/server-action/edge"]); + }); + }); +}); diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts new file mode 100644 index 00000000000..9a1c48a1d71 --- /dev/null +++ b/src/frameworks/next/utils.ts @@ -0,0 +1,491 @@ +import { existsSync } from "fs"; +import { pathExists } from "fs-extra"; +import { basename, extname, join, posix, sep } from "path"; +import { readFile } from "fs/promises"; +import { glob, sync as globSync } from "glob"; +import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin"; +import { coerce } from "semver"; + +import { findDependency, isUrl, readJSON } from "../utils"; +import type { + RoutesManifest, + ExportMarker, + ImagesManifest, + NpmLsDepdendency, + RoutesManifestRewrite, + RoutesManifestRedirect, + RoutesManifestHeader, + MiddlewareManifest, + MiddlewareManifestV1, + MiddlewareManifestV2, + AppPathsManifest, + HostingHeadersWithSource, + AppPathRoutesManifest, + ActionManifest, + NextConfigFileName, +} from "./interfaces"; +import { + APP_PATH_ROUTES_MANIFEST, + EXPORT_MARKER, + IMAGES_MANIFEST, + MIDDLEWARE_MANIFEST, + WEBPACK_LAYERS, + CONFIG_FILES, +} from "./constants"; +import { dirExistsSync, fileExistsSync } from "../../fsutils"; +import { IS_WINDOWS } from "../../utils"; + +export const I18N_SOURCE = /\/:nextInternalLocale(\([^\)]+\))?/; + +/** + * Remove escaping from characters used for Regex patch matching that Next.js + * requires. As Firebase Hosting does not require escaping for those charachters, + * we remove them. + * + * According to the Next.js documentation: + * ```md + * The following characters (, ), {, }, :, *, +, ? are used for regex path + * matching, so when used in the source as non-special values they must be + * escaped by adding \\ before them. + * ``` + * + * See: https://nextjs.org/docs/api-reference/next.config.js/rewrites#regex-path-matching + */ +export function cleanEscapedChars(path: string): string { + return path.replace(/\\([(){}:+?*])/g, (a, b: string) => b); +} + +/** + * Remove Next.js internal i18n prefix from headers, redirects and rewrites. + */ +export function cleanCustomRouteI18n(path: string): string { + return path.replace(I18N_SOURCE, ""); +} + +export function cleanI18n(it: T & { source: string; [key: string]: any }): T { + const [, localesRegex] = it.source.match(I18N_SOURCE) || [undefined, undefined]; + const source = localesRegex ? cleanCustomRouteI18n(it.source) : it.source; + const destination = + "destination" in it && localesRegex ? cleanCustomRouteI18n(it.destination) : it.destination; + const regex = + "regex" in it && localesRegex ? it.regex.replace(`(?:/${localesRegex})`, "") : it.regex; + return { + ...it, + source, + destination, + regex, + }; +} + +/** + * Whether a Next.js rewrite is supported by `firebase.json`. + * + * See: https://firebase.google.com/docs/hosting/full-config#rewrites + * + * Next.js unsupported rewrites includes: + * - Rewrites with the `has` or `missing` property that is used by Next.js for Header, + * Cookie, and Query Matching. + * - https://nextjs.org/docs/api-reference/next.config.js/rewrites#header-cookie-and-query-matching + * + * - Rewrites to external URLs or URLs using parameters + */ +export function isRewriteSupportedByHosting(rewrite: RoutesManifestRewrite): boolean { + return !( + "has" in rewrite || + "missing" in rewrite || + isUrl(rewrite.destination) || + rewrite.destination.includes("?") + ); +} + +/** + * Whether a Next.js redirect is supported by `firebase.json`. + * + * See: https://firebase.google.com/docs/hosting/full-config#redirects + * + * Next.js unsupported redirects includes: + * - Redirects with the `has` or `missing` property that is used by Next.js for Header, + * Cookie, and Query Matching. + * - https://nextjs.org/docs/api-reference/next.config.js/redirects#header-cookie-and-query-matching + * + * - Next.js internal redirects + */ +export function isRedirectSupportedByHosting(redirect: RoutesManifestRedirect): boolean { + return !( + "has" in redirect || + "missing" in redirect || + "internal" in redirect || + redirect.destination.includes("?") + ); +} + +/** + * Whether a Next.js custom header is supported by `firebase.json`. + * + * See: https://firebase.google.com/docs/hosting/full-config#headers + * + * Next.js unsupported headers includes: + * - Custom header with the `has` or `missing` property that is used by Next.js for Header, + * Cookie, and Query Matching. + * - https://nextjs.org/docs/api-reference/next.config.js/headers#header-cookie-and-query-matching + * + */ +export function isHeaderSupportedByHosting(header: RoutesManifestHeader): boolean { + return !("has" in header || "missing" in header); +} + +/** + * Get which Next.js rewrites will be used before checking supported items individually. + * + * Next.js rewrites can be arrays or objects: + * - For arrays, all supported items can be used. + * - For objects only `beforeFiles` can be used. + * + * See: https://nextjs.org/docs/api-reference/next.config.js/rewrites + */ +export function getNextjsRewritesToUse( + nextJsRewrites: RoutesManifest["rewrites"], +): RoutesManifestRewrite[] { + if (Array.isArray(nextJsRewrites)) { + return nextJsRewrites.map(cleanI18n); + } + + if (nextJsRewrites?.beforeFiles) { + return nextJsRewrites.beforeFiles.map(cleanI18n); + } + + return []; +} + +/** + * Check if `/app` directory is used in the Next.js project. + * @param sourceDir location of the source directory + * @return true if app directory is used in the Next.js project + */ +export function usesAppDirRouter(sourceDir: string): boolean { + const appPathRoutesManifestPath = join(sourceDir, APP_PATH_ROUTES_MANIFEST); + return existsSync(appPathRoutesManifestPath); +} + +/** + * Check if the project is using the next/image component based on the export-marker.json file. + * @param sourceDir location of the source directory + * @return true if the Next.js project uses the next/image component + */ +export async function usesNextImage(sourceDir: string, distDir: string): Promise { + const exportMarker = await readJSON(join(sourceDir, distDir, EXPORT_MARKER)); + return exportMarker.isNextImageImported; +} + +/** + * Check if Next.js is forced to serve the source image as-is instead of being oprimized + * by setting `unoptimized: true` in next.config.js. + * https://nextjs.org/docs/api-reference/next/image#unoptimized + * + * @param sourceDir location of the source directory + * @param distDir location of the dist directory + * @return true if image optimization is disabled + */ +export async function hasUnoptimizedImage(sourceDir: string, distDir: string): Promise { + const imagesManifest = await readJSON(join(sourceDir, distDir, IMAGES_MANIFEST)); + return imagesManifest.images.unoptimized; +} + +/** + * Whether Next.js middleware is being used + * + * @param dir in development must be the project root path, otherwise `distDir` + * @param isDevMode whether the project is running on dev or production + */ +export async function isUsingMiddleware(dir: string, isDevMode: boolean): Promise { + if (isDevMode) { + const [middlewareJs, middlewareTs] = await Promise.all([ + pathExists(join(dir, "middleware.js")), + pathExists(join(dir, "middleware.ts")), + ]); + + return middlewareJs || middlewareTs; + } else { + const middlewareManifest: MiddlewareManifest = await readJSON( + join(dir, "server", MIDDLEWARE_MANIFEST), + ); + + return Object.keys(middlewareManifest.middleware).length > 0; + } +} + +/** + * Whether image optimization is being used + * + * @param projectDir path to the project directory + * @param distDir path to `distDir` - where the manifests are located + */ +export async function isUsingImageOptimization( + projectDir: string, + distDir: string, +): Promise { + let isNextImageImported = await usesNextImage(projectDir, distDir); + + // App directory doesn't use the export marker, look it up manually + if (!isNextImageImported && isUsingAppDirectory(join(projectDir, distDir))) { + if (await isUsingNextImageInAppDirectory(projectDir, distDir)) { + isNextImageImported = true; + } + } + + if (isNextImageImported) { + const imagesManifest = await readJSON( + join(projectDir, distDir, IMAGES_MANIFEST), + ); + return !imagesManifest.images.unoptimized; + } + + return false; +} + +/** + * Whether next/image is being used in the app directory + */ +export async function isUsingNextImageInAppDirectory( + projectDir: string, + nextDir: string, +): Promise { + const nextImagePath = ["node_modules", "next", "dist", "client", "image"]; + const nextImageString = IS_WINDOWS + ? // Note: Windows requires double backslashes to match Next.js generated file + nextImagePath.join(sep + sep) + : join(...nextImagePath); + + const files = globSync( + join(projectDir, nextDir, "server", "**", "*client-reference-manifest.js"), + ); + + for (const filepath of files) { + const fileContents = await readFile(filepath, "utf-8"); + + // Return true when the first file containing the next/image component is found + if (fileContents.includes(nextImageString)) { + return true; + } + } + + return false; +} + +/** + * Whether Next.js app directory is being used + * + * @param dir path to `distDir` - where the manifests are located + */ +export function isUsingAppDirectory(dir: string): boolean { + const appPathRoutesManifestPath = join(dir, APP_PATH_ROUTES_MANIFEST); + + return fileExistsSync(appPathRoutesManifestPath); +} + +/** + * Given input from `npm ls` flatten the dependency tree and return all module names + * + * @param dependencies returned from `npm ls` + */ +export function allDependencyNames(mod: NpmLsDepdendency): string[] { + if (!mod.dependencies) return []; + const dependencyNames = Object.keys(mod.dependencies).reduce( + (acc, it) => [...acc, it, ...allDependencyNames(mod.dependencies![it])], + [] as string[], + ); + return dependencyNames; +} + +/** + * Get regexes from middleware matcher manifest + */ +export function getMiddlewareMatcherRegexes(middlewareManifest: MiddlewareManifest): RegExp[] { + const middlewareObjectValues = Object.values(middlewareManifest.middleware); + + let middlewareMatchers: Record<"regexp", string>[]; + + if (middlewareManifest.version === 1) { + middlewareMatchers = middlewareObjectValues.map( + (page: MiddlewareManifestV1["middleware"]["page"]) => ({ regexp: page.regexp }), + ); + } else { + middlewareMatchers = middlewareObjectValues + .map((page: MiddlewareManifestV2["middleware"]["page"]) => page.matchers) + .flat(); + } + + return middlewareMatchers.map((matcher) => new RegExp(matcher.regexp)); +} + +/** + * Get non static routes based on pages-manifest, prerendered and dynamic routes + */ +export function getNonStaticRoutes( + pagesManifestJSON: PagesManifest, + prerenderedRoutes: string[], + dynamicRoutes: string[], +): string[] { + const nonStaticRoutes = Object.entries(pagesManifestJSON) + .filter( + ([it, src]) => + !( + extname(src) !== ".js" || + ["/_app", "/_error", "/_document"].includes(it) || + prerenderedRoutes.includes(it) || + dynamicRoutes.includes(it) + ), + ) + .map(([it]) => it); + + return nonStaticRoutes; +} + +/** + * Get non static components from app directory + */ +export function getNonStaticServerComponents( + appPathsManifest: AppPathsManifest, + appPathRoutesManifest: AppPathRoutesManifest, + prerenderedRoutes: string[], + dynamicRoutes: string[], +): Set { + const nonStaticServerComponents = Object.entries(appPathsManifest) + .filter(([it, src]) => { + if (extname(src) !== ".js") return; + const path = appPathRoutesManifest[it]; + return !(prerenderedRoutes.includes(path) || dynamicRoutes.includes(path)); + }) + .map(([it]) => it); + + return new Set(nonStaticServerComponents); +} + +/** + * Get headers from .meta files + */ +export async function getHeadersFromMetaFiles( + sourceDir: string, + distDir: string, + basePath: string, + appPathRoutesManifest: AppPathRoutesManifest, +): Promise { + const headers: HostingHeadersWithSource[] = []; + + await Promise.all( + Object.entries(appPathRoutesManifest).map(async ([key, source]) => { + if (!["route", "page"].includes(basename(key))) return; + const parts = source.split("/").filter((it) => !!it); + const partsOrIndex = parts.length > 0 ? parts : ["index"]; + + const routePath = join(sourceDir, distDir, "server", "app", ...partsOrIndex); + const metadataPath = `${routePath}.meta`; + + if (dirExistsSync(routePath) && fileExistsSync(metadataPath)) { + const meta = await readJSON<{ headers?: Record }>(metadataPath); + if (meta.headers) + headers.push({ + source: posix.join(basePath, source), + headers: Object.entries(meta.headers).map(([key, value]) => ({ key, value })), + }); + } + }), + ); + + return headers; +} + +/** + * Get build id from .next/BUILD_ID file + * @throws if file doesn't exist + */ +export async function getBuildId(distDir: string): Promise { + const buildId = await readFile(join(distDir, "BUILD_ID")); + + return buildId.toString(); +} + +/** + * Get Next.js version in the following format: `major.minor.patch`, ignoring + * canary versions as it causes issues with semver comparisons. + */ +export function getNextVersion(cwd: string): string | undefined { + const dependency = findDependency("next", { cwd, depth: 0, omitDev: false }); + if (!dependency) return undefined; + + const nextVersionSemver = coerce(dependency.version); + if (!nextVersionSemver) return dependency.version; + + return nextVersionSemver.toString(); +} + +/** + * Whether the Next.js project has a static `not-found` page in the app directory. + * + * The Next.js build manifests are misleading regarding the existence of a static + * `not-found` component. Therefore, we check if a `_not-found.html` file exists + * in the generated app directory files to know whether `not-found` is static. + */ +export async function hasStaticAppNotFoundComponent( + sourceDir: string, + distDir: string, +): Promise { + return pathExists(join(sourceDir, distDir, "server", "app", "_not-found.html")); +} + +/** + * Find routes using server actions by checking the server-reference-manifest.json + */ +export function getRoutesWithServerAction( + serverReferenceManifest: ActionManifest, + appPathRoutesManifest: AppPathRoutesManifest, +): string[] { + const routesWithServerAction = new Set(); + + for (const key of Object.keys(serverReferenceManifest)) { + if (key !== "edge" && key !== "node") continue; + + const edgeOrNode = serverReferenceManifest[key]; + + for (const actionId of Object.keys(edgeOrNode)) { + if (!edgeOrNode[actionId].layer) continue; + + for (const [route, type] of Object.entries(edgeOrNode[actionId].layer)) { + if (type === WEBPACK_LAYERS.actionBrowser) { + routesWithServerAction.add(appPathRoutesManifest[route.replace("app", "")]); + } + } + } + } + + return Array.from(routesWithServerAction); +} + +/** + * Get files in the dist directory to be deployed to Firebase, ignoring development files. + * + * Return relative paths to the dist directory. + */ +export async function getProductionDistDirFiles( + sourceDir: string, + distDir: string, +): Promise { + return glob("**", { + ignore: [join("cache", "webpack", "*-development", "**"), join("cache", "eslint", "**")], + cwd: join(sourceDir, distDir), + nodir: true, + absolute: false, + }); +} + +/** + * Get the Next.js config file name in the project directory, either + * `next.config.js` or `next.config.mjs`. If none of them exist, return null. + */ +export async function whichNextConfigFile(dir: string): Promise { + for (const file of CONFIG_FILES) { + if (await pathExists(join(dir, file))) return file; + } + + return null; +} diff --git a/src/frameworks/nuxt/index.spec.ts b/src/frameworks/nuxt/index.spec.ts new file mode 100644 index 00000000000..9e294e9e35c --- /dev/null +++ b/src/frameworks/nuxt/index.spec.ts @@ -0,0 +1,122 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { EventEmitter } from "events"; +import type { ChildProcess } from "child_process"; +import { Readable, Writable } from "stream"; +import * as fsExtra from "fs-extra"; +import * as crossSpawn from "cross-spawn"; + +import * as frameworksUtils from "../utils"; +import { discover as discoverNuxt2 } from "../nuxt2"; +import { discover as discoverNuxt3, getDevModeHandle } from "."; +import type { NuxtOptions } from "./interfaces"; + +describe("Nuxt 2 utils", () => { + describe("nuxtAppDiscovery", () => { + const discoverNuxtDir = "."; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should find a Nuxt 2 app", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(true); + sandbox.stub(frameworksUtils, "findDependency").returns({ + version: "2.15.8", + resolved: "https://registry.npmjs.org/nuxt/-/nuxt-2.15.8.tgz", + overridden: false, + }); + sandbox + .stub(frameworksUtils, "relativeRequire") + .withArgs(discoverNuxtDir, "nuxt/dist/nuxt.js" as any) + .resolves({ + loadNuxt: () => + Promise.resolve({ + ready: () => Promise.resolve(), + options: { dir: { static: "static" } }, + }), + }); + + expect(await discoverNuxt2(discoverNuxtDir)).to.deep.equal({ + mayWantBackend: true, + version: "2.15.8", + }); + }); + + it("should find a Nuxt 3 app", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(true); + sandbox.stub(frameworksUtils, "findDependency").returns({ + version: "3.0.0", + resolved: "https://registry.npmjs.org/nuxt/-/nuxt-3.0.0.tgz", + overridden: false, + }); + sandbox + .stub(frameworksUtils, "relativeRequire") + .withArgs(discoverNuxtDir, "@nuxt/kit") + .resolves({ + loadNuxtConfig: async function (): Promise { + return Promise.resolve({ + ssr: true, + app: { + baseURL: "/", + }, + dir: { + public: "public", + }, + }); + }, + }); + + expect(await discoverNuxt3(discoverNuxtDir)).to.deep.equal({ + mayWantBackend: true, + version: "3.0.0", + }); + }); + }); + + describe("getDevModeHandle", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should resolve with initial Nuxt 3 dev server output", async () => { + const process = new EventEmitter() as ChildProcess; + process.stdin = new Writable(); + process.stdout = new EventEmitter() as Readable; + process.stderr = new EventEmitter() as Readable; + + const cli = Math.random().toString(36).split(".")[1]; + sandbox.stub(frameworksUtils, "getNodeModuleBin").withArgs("nuxt", ".").returns(cli); + sandbox.stub(crossSpawn, "spawn").withArgs(cli, ["dev"], { cwd: "." }).returns(process); + + const devModeHandle = getDevModeHandle("."); + + process.stdout.emit( + "data", + `Nuxi 3.0.0 + + WARN Changing NODE_ENV from production to development, to avoid unintended behavior. + + Nuxt 3.0.0 with Nitro 1.0.0 + + > Local: http://localhost:3000/ + > Network: http://0.0.0.0:3000/ + > Network: http://[some:ipv6::::::]:3000/ + > Network: http://[some:other:ipv6:::::]:3000/`, + ); + + await expect(devModeHandle).eventually.be.fulfilled; + }); + }); +}); diff --git a/src/frameworks/nuxt/index.ts b/src/frameworks/nuxt/index.ts new file mode 100644 index 00000000000..b478b365701 --- /dev/null +++ b/src/frameworks/nuxt/index.ts @@ -0,0 +1,124 @@ +import { copy, mkdirp, pathExists } from "fs-extra"; +import { readFile } from "fs/promises"; +import { join, posix } from "path"; +import { lt } from "semver"; +import { spawn, sync as spawnSync } from "cross-spawn"; +import { FrameworkType, SupportLevel } from "../interfaces"; +import { simpleProxy, warnIfCustomBuildScript, getNodeModuleBin, relativeRequire } from "../utils"; +import { getNuxtVersion } from "./utils"; + +export const name = "Nuxt"; +export const support = SupportLevel.Experimental; +export const type = FrameworkType.Toolchain; +export const supportedRange = "3"; + +import { nuxtConfigFilesExist } from "./utils"; +import type { NuxtOptions } from "./interfaces"; +import { FirebaseError } from "../../error"; +import { execSync } from "child_process"; + +const DEFAULT_BUILD_SCRIPT = ["nuxt build", "nuxi build"]; + +/** + * + * @param dir current directory + * @return undefined if project is not Nuxt 2, { mayWantBackend: true, publicDirectory: string } otherwise + */ +export async function discover(dir: string) { + if (!(await pathExists(join(dir, "package.json")))) return; + + const anyConfigFileExists = await nuxtConfigFilesExist(dir); + + const version = getNuxtVersion(dir); + if (!anyConfigFileExists && !version) return; + if (version && lt(version, "3.0.0-0")) return; + + const { ssr: mayWantBackend } = await getConfig(dir); + + return { mayWantBackend, version }; +} + +export async function build(cwd: string) { + await warnIfCustomBuildScript(cwd, name, DEFAULT_BUILD_SCRIPT); + const cli = getNodeModuleBin("nuxt", cwd); + const { + ssr: wantsBackend, + app: { baseURL: baseUrl }, + } = await getConfig(cwd); + const command = wantsBackend ? ["build"] : ["generate"]; + const build = spawnSync(cli, command, { + cwd, + stdio: "inherit", + env: { ...process.env, NITRO_PRESET: "node" }, + }); + if (build.status !== 0) throw new FirebaseError("Was unable to build your Nuxt application."); + const rewrites = wantsBackend + ? [] + : [ + { + source: posix.join(baseUrl, "**"), + destination: posix.join(baseUrl, "200.html"), + }, + ]; + return { wantsBackend, rewrites, baseUrl }; +} + +export async function ɵcodegenPublicDirectory(root: string, dest: string) { + const { + app: { baseURL }, + } = await getConfig(root); + const distPath = join(root, ".output", "public"); + const fullDest = join(dest, baseURL); + await mkdirp(fullDest); + await copy(distPath, fullDest); +} + +export async function ɵcodegenFunctionsDirectory(sourceDir: string) { + const serverDir = join(sourceDir, ".output", "server"); + const packageJsonBuffer = await readFile(join(sourceDir, "package.json")); + const packageJson = JSON.parse(packageJsonBuffer.toString()); + + packageJson.dependencies ||= {}; + packageJson.dependencies["nitro-output"] = `file:${serverDir}`; + + return { packageJson, frameworksEntry: "nitro" }; +} + +export async function getDevModeHandle(cwd: string) { + const host = new Promise((resolve, reject) => { + const cli = getNodeModuleBin("nuxt", cwd); + const serve = spawn(cli, ["dev"], { cwd: cwd }); + + serve.stdout.on("data", (data: any) => { + process.stdout.write(data); + const match = data.toString().match(/(http:\/\/.+:\d+)/); + + if (match) resolve(match[1]); + }); + + serve.stderr.on("data", (data: any) => { + process.stderr.write(data); + }); + + serve.on("exit", reject); + }); + + return simpleProxy(await host); +} + +export async function getConfig(cwd: string): Promise { + const { loadNuxtConfig } = await relativeRequire(cwd, "@nuxt/kit"); + + return await loadNuxtConfig({ cwd }); +} + +/** + * Utility method used during project initialization. + */ +export function init(setup: any, config: any) { + execSync(`npx --yes nuxi@"${supportedRange}" init ${setup.hosting.source}`, { + stdio: "inherit", + cwd: config.projectDir, + }); + return Promise.resolve(); +} diff --git a/src/frameworks/nuxt/interfaces.ts b/src/frameworks/nuxt/interfaces.ts new file mode 100644 index 00000000000..b8d02cf19c0 --- /dev/null +++ b/src/frameworks/nuxt/interfaces.ts @@ -0,0 +1,12 @@ +// TODO: define more fields as needed +// The NuxtOptions interface is huge and depends on multiple external types +// and packages. For now only the fields that are being used are defined. +export interface NuxtOptions { + ssr: boolean; + app: { + baseURL: string; + }; + dir: { + public: string; + }; +} diff --git a/src/frameworks/nuxt/utils.ts b/src/frameworks/nuxt/utils.ts new file mode 100644 index 00000000000..38c20416ef7 --- /dev/null +++ b/src/frameworks/nuxt/utils.ts @@ -0,0 +1,25 @@ +import { pathExists } from "fs-extra"; +import { join } from "path"; +import { findDependency } from "../utils"; + +export function getNuxtVersion(cwd: string): string | undefined { + return findDependency("nuxt", { + cwd, + depth: 0, + omitDev: false, + })?.version; +} + +/** + * + * @param dir current app directory + * @return true or false if Nuxt config file was found in the directory + */ +export async function nuxtConfigFilesExist(dir: string): Promise { + const configFilesExist = await Promise.all([ + pathExists(join(dir, "nuxt.config.js")), + pathExists(join(dir, "nuxt.config.ts")), + ]); + + return configFilesExist.some((it) => it); +} diff --git a/src/frameworks/nuxt2/index.ts b/src/frameworks/nuxt2/index.ts new file mode 100644 index 00000000000..c67178c95dd --- /dev/null +++ b/src/frameworks/nuxt2/index.ts @@ -0,0 +1,119 @@ +import { copy, pathExists } from "fs-extra"; +import { readFile } from "fs/promises"; +import { basename, join, relative } from "path"; +import { gte } from "semver"; + +import { SupportLevel, FrameworkType } from "../interfaces"; +import { getNodeModuleBin, relativeRequire } from "../utils"; +import { getNuxtVersion } from "../nuxt/utils"; +import { simpleProxy } from "../utils"; +import { spawn } from "cross-spawn"; + +export const name = "Nuxt"; +export const support = SupportLevel.Experimental; +export const type = FrameworkType.MetaFramework; +export const supportedRange = "2"; + +async function getAndLoadNuxt(options: { rootDir: string; for: string }) { + const nuxt = await relativeRequire(options.rootDir, "nuxt/dist/nuxt.js"); + const app = await nuxt.loadNuxt(options); + await app.ready(); + return { app, nuxt }; +} + +/** + * + * @param rootDir current directory + * @return undefined if project is not Nuxt 2, {mayWantBackend: true } otherwise + */ +export async function discover(rootDir: string) { + if (!(await pathExists(join(rootDir, "package.json")))) return; + const version = getNuxtVersion(rootDir); + if (!version || (version && gte(version, "3.0.0-0"))) return; + return { mayWantBackend: true, version }; +} + +/** + * + * @param rootDir nuxt project root + * @return whether backend is needed or not + */ +export async function build(rootDir: string) { + const { app, nuxt } = await getAndLoadNuxt({ rootDir, for: "build" }); + const { + options: { ssr, target }, + } = app; + + // Nuxt seems to use process.cwd() somewhere + const cwd = process.cwd(); + process.chdir(rootDir); + + await nuxt.build(app); + const { app: generateApp } = await getAndLoadNuxt({ rootDir, for: "start" }); + const builder = await nuxt.getBuilder(generateApp); + const generator = new nuxt.Generator(generateApp, builder); + await generator.generate({ build: false, init: true }); + + process.chdir(cwd); + + const wantsBackend = ssr && target === "server"; + const rewrites = wantsBackend ? [] : [{ source: "**", destination: "/200.html" }]; + + return { wantsBackend, rewrites }; +} + +/** + * Copy the static files to the destination directory whether it's a static build or server build. + * @param rootDir + * @param dest + */ +export async function ɵcodegenPublicDirectory(rootDir: string, dest: string) { + const { + app: { options }, + } = await getAndLoadNuxt({ rootDir, for: "build" }); + await copy(options.generate.dir, dest); +} + +export async function ɵcodegenFunctionsDirectory(rootDir: string, destDir: string) { + const packageJsonBuffer = await readFile(join(rootDir, "package.json")); + const packageJson = JSON.parse(packageJsonBuffer.toString()); + + // Get the nuxt config into an object so we can check the `target` and `ssr` properties. + const { + app: { options }, + } = await getAndLoadNuxt({ rootDir, for: "build" }); + const { buildDir, _nuxtConfigFile: configFilePath } = options; + + // When starting the Nuxt 2 server, we need to copy the `.nuxt` to the destination directory (`functions`) + // with the same folder name (.firebase//functions/.nuxt). + // This is because `loadNuxt` (called from `firebase-frameworks`) will only look + // for the `.nuxt` directory in the destination directory. + await copy(buildDir, join(destDir, relative(rootDir, buildDir))); + + // TODO pack this + await copy(configFilePath, join(destDir, basename(configFilePath))); + + return { packageJson: { ...packageJson }, frameworksEntry: "nuxt" }; +} + +export async function getDevModeHandle(cwd: string) { + const host = new Promise((resolve, reject) => { + const cli = getNodeModuleBin("nuxt", cwd); + const serve = spawn(cli, ["dev"], { cwd }); + + serve.stdout.on("data", (data: any) => { + process.stdout.write(data); + const match = data.toString().match(/(http:\/\/.+:\d+)/); + + if (match) resolve(match[1]); + }); + + serve.stderr.on("data", (data: any) => { + process.stderr.write(data); + }); + + serve.on("exit", reject); + }); + + return simpleProxy(await host); +} diff --git a/src/frameworks/preact/index.ts b/src/frameworks/preact/index.ts new file mode 100644 index 00000000000..595b1e2933c --- /dev/null +++ b/src/frameworks/preact/index.ts @@ -0,0 +1,10 @@ +import { FrameworkType } from "../interfaces"; +import { initViteTemplate, vitePluginDiscover } from "../vite"; + +export * from "../vite"; + +export const name = "Preact"; +export const type = FrameworkType.Framework; + +export const init = initViteTemplate("preact"); +export const discover = vitePluginDiscover("vite:preact-jsx"); diff --git a/src/frameworks/react/index.ts b/src/frameworks/react/index.ts new file mode 100644 index 00000000000..686adfd6c66 --- /dev/null +++ b/src/frameworks/react/index.ts @@ -0,0 +1,10 @@ +import { FrameworkType } from "../interfaces"; +import { initViteTemplate, vitePluginDiscover } from "../vite"; + +export * from "../vite"; + +export const name = "React"; +export const type = FrameworkType.Framework; + +export const init = initViteTemplate("react"); +export const discover = vitePluginDiscover("vite:react-jsx"); diff --git a/src/frameworks/svelte/index.ts b/src/frameworks/svelte/index.ts new file mode 100644 index 00000000000..31efd3dadcd --- /dev/null +++ b/src/frameworks/svelte/index.ts @@ -0,0 +1,10 @@ +import { FrameworkType } from "../interfaces"; +import { initViteTemplate, vitePluginDiscover } from "../vite"; + +export * from "../vite"; + +export const name = "Svelte"; +export const type = FrameworkType.Framework; + +export const init = initViteTemplate("svelte"); +export const discover = vitePluginDiscover("vite-plugin-svelte"); diff --git a/src/frameworks/sveltekit/index.ts b/src/frameworks/sveltekit/index.ts new file mode 100644 index 00000000000..c2d233feb31 --- /dev/null +++ b/src/frameworks/sveltekit/index.ts @@ -0,0 +1,55 @@ +import { copy, pathExists, readFile } from "fs-extra"; +import { join } from "path"; +import { FrameworkType, SupportLevel } from "../interfaces"; +import { viteDiscoverWithNpmDependency, build as viteBuild } from "../vite"; +import { SvelteKitConfig } from "./interfaces"; +import { fileExistsSync } from "../../fsutils"; + +const { dynamicImport } = require(true && "../../dynamicImport"); + +export const name = "SvelteKit"; +export const support = SupportLevel.Experimental; +export const type = FrameworkType.MetaFramework; +export const discover = viteDiscoverWithNpmDependency("@sveltejs/kit"); + +export { getDevModeHandle, supportedRange } from "../vite"; + +export async function build(root: string, target: string) { + const config = await getConfig(root); + const wantsBackend = config.kit.adapter?.name !== "@sveltejs/adapter-static"; + await viteBuild(root, target); + return { wantsBackend }; +} + +export async function ɵcodegenPublicDirectory(root: string, dest: string) { + const config = await getConfig(root); + const output = join(root, config.kit.outDir, "output"); + await copy(join(output, "client"), dest); + + const prerenderedPath = join(output, "prerendered", "pages"); + if (await pathExists(prerenderedPath)) { + await copy(prerenderedPath, dest); + } +} + +export async function ɵcodegenFunctionsDirectory(sourceDir: string, destDir: string) { + const packageJsonBuffer = await readFile(join(sourceDir, "package.json")); + const packageJson = JSON.parse(packageJsonBuffer.toString()); + packageJson.dependencies ||= {}; + packageJson.dependencies["@sveltejs/kit"] ??= packageJson.devDependencies["@sveltejs/kit"]; + + const config = await getConfig(sourceDir); + await copy(join(sourceDir, config.kit.outDir, "output", "server"), destDir); + + return { packageJson, frameworksEntry: "sveltekit" }; +} + +async function getConfig(root: string): Promise { + const configPath = ["svelte.config.js", "svelte.config.mjs"] + .map((filename) => join(root, filename)) + .find(fileExistsSync); + const config = configPath ? (await dynamicImport(configPath)).default : {}; + config.kit ||= {}; + config.kit.outDir ||= ".svelte-kit"; + return config; +} diff --git a/src/frameworks/sveltekit/interfaces.ts b/src/frameworks/sveltekit/interfaces.ts new file mode 100644 index 00000000000..3e371d60e10 --- /dev/null +++ b/src/frameworks/sveltekit/interfaces.ts @@ -0,0 +1,8 @@ +export interface SvelteKitConfig { + kit: { + outDir: string; + adapter?: { + name: string; + }; + }; +} diff --git a/src/frameworks/utils.spec.ts b/src/frameworks/utils.spec.ts new file mode 100644 index 00000000000..ab424dfcb6f --- /dev/null +++ b/src/frameworks/utils.spec.ts @@ -0,0 +1,138 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as fs from "fs"; +import { resolve, join } from "path"; + +import { warnIfCustomBuildScript, isUrl, getNodeModuleBin, conjoinOptions } from "./utils"; + +describe("Frameworks utils", () => { + describe("getNodeModuleBin", () => { + it("should return expected tsc path", () => { + expect(getNodeModuleBin("tsc", __dirname)).to.equal( + resolve(join(__dirname, "..", "..", "node_modules", ".bin", "tsc")), + ); + }).timeout(5000); + it("should throw when npm root not found", () => { + expect(() => { + getNodeModuleBin("tsc", "/"); + }).to.throw("Could not find the tsc executable."); + }).timeout(5000); + it("should throw when executable not found", () => { + expect(() => { + getNodeModuleBin("xxxxx", __dirname); + }).to.throw("Could not find the xxxxx executable."); + }).timeout(5000); + }); + + describe("isUrl", () => { + it("should identify http URL", () => { + expect(isUrl("http://firebase.google.com")).to.be.true; + }); + + it("should identify https URL", () => { + expect(isUrl("https://firebase.google.com")).to.be.true; + }); + + it("should ignore URL within path", () => { + expect(isUrl("path/?url=https://firebase.google.com")).to.be.false; + }); + + it("should ignore path starting with http but without protocol", () => { + expect(isUrl("httpendpoint/foo/bar")).to.be.false; + }); + + it("should ignore path starting with https but without protocol", () => { + expect(isUrl("httpsendpoint/foo/bar")).to.be.false; + }); + }); + + describe("warnIfCustomBuildScript", () => { + const framework = "Next.js"; + let sandbox: sinon.SinonSandbox; + let consoleLogSpy: sinon.SinonSpy; + const packageJson = { + scripts: { + build: "", + }, + }; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + consoleLogSpy = sandbox.spy(console, "warn"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should not print warning when a default build script is found.", async () => { + const buildScript = "next build"; + const defaultBuildScripts = ["next build"]; + packageJson.scripts.build = buildScript; + + sandbox.stub(fs.promises, "readFile").resolves(JSON.stringify(packageJson)); + + await warnIfCustomBuildScript("fakedir/", framework, defaultBuildScripts); + + expect(consoleLogSpy.callCount).to.equal(0); + }); + + it("should print warning when a custom build script is found.", async () => { + const buildScript = "echo 'Custom build script' && next build"; + const defaultBuildScripts = ["next build"]; + packageJson.scripts.build = buildScript; + + sandbox.stub(fs.promises, "readFile").resolves(JSON.stringify(packageJson)); + + await warnIfCustomBuildScript("fakedir/", framework, defaultBuildScripts); + + expect(consoleLogSpy).to.be.calledOnceWith( + `\nWARNING: Your package.json contains a custom build that is being ignored. Only the ${framework} default build script (e.g, "${defaultBuildScripts[0]}") is respected. If you have a more advanced build process you should build a custom integration https://firebase.google.com/docs/hosting/express\n`, + ); + }); + }); + + describe("conjoinOptions", () => { + const options = [14, 16, 18]; + const defaultSeparator = ","; + const defaultConjunction = "and"; + + it("should return empty string if there's no options", () => { + expect(conjoinOptions([])).to.be.eql(""); + }); + + it("should return option if there's only one", () => { + expect(conjoinOptions([options[0]])).to.equal(options[0].toString()); + }); + + it("should return options without separator if there's two options", () => { + const twoOptions = options.slice(0, 2); + + expect(conjoinOptions(twoOptions)).to.equal( + `${twoOptions[0]} ${defaultConjunction} ${twoOptions[1]}`, + ); + }); + + it("should return options with default conjunction and default separator", () => { + expect(conjoinOptions(options)).to.equal( + `${options[0]}${defaultSeparator} ${options[1]}${defaultSeparator} ${defaultConjunction} ${options[2]}`, + ); + }); + + it("should return options with custom separator", () => { + const customSeparator = "/"; + + expect(conjoinOptions(options, defaultConjunction, customSeparator)).to.equal( + `${options[0]}${customSeparator} ${options[1]}${customSeparator} ${defaultConjunction} ${options[2]}`, + ); + }); + + it("should return options with custom conjunction", () => { + const customConjuntion = "or"; + + expect(conjoinOptions(options, customConjuntion, defaultSeparator)).to.equal( + `${options[0]}${defaultSeparator} ${options[1]}${defaultSeparator} ${customConjuntion} ${options[2]}`, + ); + }); + }); +}); diff --git a/src/frameworks/utils.ts b/src/frameworks/utils.ts new file mode 100644 index 00000000000..2bb8a9a0bd5 --- /dev/null +++ b/src/frameworks/utils.ts @@ -0,0 +1,465 @@ +import { readJSON as originalReadJSON } from "fs-extra"; +import type { ReadOptions } from "fs-extra"; +import { dirname, extname, join, relative } from "path"; +import { readFile } from "fs/promises"; +import { IncomingMessage, request as httpRequest, ServerResponse, Agent } from "http"; +import { sync as spawnSync } from "cross-spawn"; +import * as clc from "colorette"; +import { satisfies as semverSatisfied } from "semver"; + +import { logger } from "../logger"; +import { FirebaseError } from "../error"; +import { fileExistsSync } from "../fsutils"; +import { pathToFileURL } from "url"; +import { + DEFAULT_DOCS_URL, + FEATURE_REQUEST_URL, + FILE_BUG_URL, + MAILING_LIST_URL, + NPM_COMMAND_TIMEOUT_MILLIES, + VALID_LOCALE_FORMATS, +} from "./constants"; +import { BUILD_TARGET_PURPOSE, PackageJson, RequestHandler } from "./interfaces"; + +// Use "true &&"" to keep typescript from compiling this file and rewriting +// the import statement into a require +const { dynamicImport } = require(true && "../dynamicImport"); + +const NPM_ROOT_TIMEOUT_MILLIES = 5_000; +const NPM_ROOT_MEMO = new Map(); + +/** + * Whether the given string starts with http:// or https:// + */ +export function isUrl(url: string): boolean { + return /^https?:\/\//.test(url); +} + +/** + * add type to readJSON + * + * Note: `throws: false` won't work with the async function: https://github.com/jprichardson/node-fs-extra/issues/542 + */ +export function readJSON( + file: string, + options?: ReadOptions | BufferEncoding | string, +): Promise { + return originalReadJSON(file, options) as Promise; +} + +/** + * Prints a warning if the build script in package.json + * contains anything other than allowedBuildScripts. + */ +export async function warnIfCustomBuildScript( + dir: string, + framework: string, + defaultBuildScripts: string[], +): Promise { + const packageJsonBuffer = await readFile(join(dir, "package.json")); + const packageJson = JSON.parse(packageJsonBuffer.toString()); + const buildScript = packageJson.scripts?.build; + + if (buildScript && !defaultBuildScripts.includes(buildScript)) { + console.warn( + `\nWARNING: Your package.json contains a custom build that is being ignored. Only the ${framework} default build script (e.g, "${defaultBuildScripts[0]}") is respected. If you have a more advanced build process you should build a custom integration https://firebase.google.com/docs/hosting/express\n`, + ); + } +} + +/** + * Proxy a HTTP response + * It uses the Proxy object to intercept the response and buffer it until the + * response is finished. This allows us to modify the response before sending + * it back to the client. + */ +export function proxyResponse( + req: IncomingMessage, + res: ServerResponse, + next: () => void, +): ServerResponse { + const proxiedRes = new ServerResponse(req); + // Object to store the original response methods + const buffer: [ + string, + Parameters, + ][] = []; + + // Proxy the response methods + // The apply handler is called when the method e.g. write, setHeader, etc. is called + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/apply + // The target is the original method + // The thisArg is the proxied response + // The args are the arguments passed to the method + proxiedRes.write = new Proxy(proxiedRes.write.bind(proxiedRes), { + apply: ( + target: ServerResponse["write"], + thisArg: ServerResponse, + args: Parameters, + ) => { + // call the original write method on the proxied response + target.call(thisArg, ...args); + // store the method call in the buffer + buffer.push(["write", args]); + }, + }); + + proxiedRes.setHeader = new Proxy(proxiedRes.setHeader.bind(proxiedRes), { + apply: ( + target: ServerResponse["setHeader"], + thisArg: ServerResponse, + args: Parameters, + ) => { + target.call(thisArg, ...args); + buffer.push(["setHeader", args]); + }, + }); + proxiedRes.removeHeader = new Proxy(proxiedRes.removeHeader.bind(proxiedRes), { + apply: ( + target: ServerResponse["removeHeader"], + thisArg: ServerResponse, + args: Parameters, + ) => { + target.call(thisArg, ...args); + buffer.push(["removeHeader", args]); + }, + }); + proxiedRes.writeHead = new Proxy(proxiedRes.writeHead.bind(proxiedRes), { + apply: ( + target: ServerResponse["writeHead"], + thisArg: ServerResponse, + args: Parameters, + ) => { + target.call(thisArg, ...args); + buffer.push(["writeHead", args]); + }, + }); + proxiedRes.end = new Proxy(proxiedRes.end.bind(proxiedRes), { + apply: ( + target: ServerResponse["end"], + thisArg: ServerResponse, + args: Parameters, + ) => { + // call the original end method on the proxied response + target.call(thisArg, ...args); + // if the proxied response is a 404, call next to continue down the middleware chain + // otherwise, send the buffered response i.e. call the original response methods: write, setHeader, etc. + // and then end the response and clear the buffer + if (proxiedRes.statusCode === 404) { + next(); + } else { + for (const [fn, args] of buffer) { + (res as any)[fn](...args); + } + res.end(...args); + buffer.length = 0; + } + }, + }); + + return proxiedRes; +} + +export function simpleProxy(hostOrRequestHandler: string | RequestHandler) { + const agent = new Agent({ keepAlive: true }); + // If the path is a the auth token sync URL pass through to Cloud Functions + const firebaseDefaultsJSON = process.env.__FIREBASE_DEFAULTS__; + const authTokenSyncURL: string | undefined = + firebaseDefaultsJSON && JSON.parse(firebaseDefaultsJSON)._authTokenSyncURL; + return async (originalReq: IncomingMessage, originalRes: ServerResponse, next: () => void) => { + const { method, headers, url: path } = originalReq; + if (!method || !path) { + originalRes.end(); + return; + } + if (path === authTokenSyncURL) { + return next(); + } + if (typeof hostOrRequestHandler === "string") { + const { hostname, port, protocol, username, password } = new URL(hostOrRequestHandler); + const host = `${hostname}:${port}`; + const auth = username || password ? `${username}:${password}` : undefined; + const opts = { + agent, + auth, + protocol, + hostname, + port, + path, + method, + headers: { + ...headers, + host, + "X-Forwarded-Host": headers.host, + }, + }; + const req = httpRequest(opts, (response) => { + const { statusCode, statusMessage, headers } = response; + if (statusCode === 404) { + next(); + } else { + originalRes.writeHead(statusCode!, statusMessage, headers); + response.pipe(originalRes); + } + }); + originalReq.pipe(req); + req.on("error", (err) => { + logger.debug("Error encountered while proxying request:", method, path, err); + originalRes.end(); + }); + } else { + const proxiedRes = proxyResponse(originalReq, originalRes, next); + await hostOrRequestHandler(originalReq, proxiedRes, next); + } + }; +} + +function scanDependencyTree(searchingFor: string, dependencies = {}): any { + for (const [name, dependency] of Object.entries( + dependencies as Record>, + )) { + if (name === searchingFor) return dependency; + const result = scanDependencyTree(searchingFor, dependency.dependencies); + if (result) return result; + } + return; +} + +export function getNpmRoot(cwd: string) { + let npmRoot = NPM_ROOT_MEMO.get(cwd); + if (npmRoot) return npmRoot; + + npmRoot = spawnSync("npm", ["root"], { + cwd, + timeout: NPM_ROOT_TIMEOUT_MILLIES, + }) + .stdout?.toString() + .trim(); + + NPM_ROOT_MEMO.set(cwd, npmRoot); + + return npmRoot; +} + +export function getNodeModuleBin(name: string, cwd: string) { + const npmRoot = getNpmRoot(cwd); + if (!npmRoot) { + throw new FirebaseError(`Error finding ${name} executable: failed to spawn 'npm'`); + } + const path = join(npmRoot, ".bin", name); + if (!fileExistsSync(path)) { + throw new FirebaseError(`Could not find the ${name} executable.`); + } + return path; +} + +interface FindDepOptions { + cwd: string; + depth?: number; + omitDev: boolean; +} + +const DEFAULT_FIND_DEP_OPTIONS: FindDepOptions = { + cwd: process.cwd(), + omitDev: true, +}; + +/** + * + */ +export function findDependency(name: string, options: Partial = {}) { + const { cwd: dir, depth, omitDev } = { ...DEFAULT_FIND_DEP_OPTIONS, ...options }; + const cwd = getNpmRoot(dir); + if (!cwd) return; + const env: any = Object.assign({}, process.env); + delete env.NODE_ENV; + const result = spawnSync( + "npm", + [ + "list", + name, + "--json=true", + ...(omitDev ? ["--omit", "dev"] : []), + ...(depth === undefined ? [] : ["--depth", depth.toString(10)]), + ], + { cwd, env, timeout: NPM_COMMAND_TIMEOUT_MILLIES }, + ); + if (!result.stdout) return; + const json = JSON.parse(result.stdout.toString()); + return scanDependencyTree(name, json.dependencies); +} + +export function relativeRequire( + dir: string, + mod: "@angular-devkit/core", +): Promise; +export function relativeRequire( + dir: string, + mod: "@angular-devkit/core/node", +): Promise; +export function relativeRequire( + dir: string, + mod: "@angular-devkit/architect", +): Promise; +export function relativeRequire( + dir: string, + mod: "@angular-devkit/architect/node", +): Promise; +export function relativeRequire( + dir: string, + mod: "next/dist/build", +): Promise; +export function relativeRequire( + dir: string, + mod: "next/dist/server/config", +): Promise; +export function relativeRequire( + dir: string, + mod: "next/constants", +): Promise; +export function relativeRequire( + dir: string, + mod: "next", +): Promise; +export function relativeRequire(dir: string, mod: "vite"): Promise; +export function relativeRequire( + dir: string, + mod: "jsonc-parser", +): Promise; + +// TODO the types for @nuxt/kit are causing a lot of troubles, need to do something other than any +// Nuxt 2 +export function relativeRequire(dir: string, mod: "nuxt/dist/nuxt.js"): Promise; +// Nuxt 3 +export function relativeRequire(dir: string, mod: "@nuxt/kit"): Promise; + +/** + * + */ +export async function relativeRequire(dir: string, mod: string) { + try { + // If being compiled with webpack, use non webpack require for these calls. + // (VSCode plugin uses webpack which by default replaces require calls + // with its own require, which doesn't work on files) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const requireFunc: typeof require = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore prevent VSCE webpack from erroring on non_webpack_require + // eslint-disable-next-line camelcase + typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore prevent VSCE webpack from erroring on non_webpack_require + const path = requireFunc.resolve(mod, { paths: [dir] }); + + let packageJson: PackageJson | undefined; + let isEsm = extname(path) === ".mjs"; + if (!isEsm) { + packageJson = await readJSON( + join(dirname(path), "package.json"), + ).catch(() => undefined); + + isEsm = packageJson?.type === "module"; + } + + if (isEsm) { + // in case path resolves to a cjs file, use main from package.json + if (extname(path) === ".cjs" && packageJson?.main) { + return dynamicImport(join(dirname(path), packageJson.main)); + } + + return dynamicImport(pathToFileURL(path).toString()); + } else { + return requireFunc(path); + } + } catch (e) { + const path = relative(process.cwd(), dir); + console.error( + `Could not load dependency ${mod} in ${ + path.startsWith("..") ? path : `./${path}` + }, have you run \`npm install\`?`, + ); + throw e; + } +} + +export function conjoinOptions(_opts: any[], conjunction = "and", separator = ","): string { + if (!_opts.length) return ""; + const opts: string[] = _opts.map((it) => it.toString().trim()); + if (opts.length === 1) return opts[0]; + if (opts.length === 2) return `${opts[0]} ${conjunction} ${opts[1]}`; + const lastElement = opts.slice(-1)[0]; + const allButLast = opts.slice(0, -1); + return `${allButLast.join(`${separator} `)}${separator} ${conjunction} ${lastElement}`; +} + +export function frameworksCallToAction( + message: string, + docsUrl = DEFAULT_DOCS_URL, + prefix = "", + framework?: string, + version?: string, + supportedRange?: string, + vite = false, +): string { + return `${prefix}${message}${ + framework && supportedRange && (!version || !semverSatisfied(version, supportedRange)) + ? clc.yellow( + `\n${prefix}The integration is known to work with ${ + vite ? "Vite" : framework + } version ${clc.italic( + conjoinOptions(supportedRange.split("||")), + )}. You may encounter errors.`, + ) + : `` + } + +${prefix}${clc.bold("Documentation:")} ${docsUrl} +${prefix}${clc.bold("File a bug:")} ${FILE_BUG_URL} +${prefix}${clc.bold("Submit a feature request:")} ${FEATURE_REQUEST_URL} + +${prefix}We'd love to learn from you. Express your interest in helping us shape the future of Firebase Hosting: ${MAILING_LIST_URL}`; +} + +export function validateLocales(locales: string[] | undefined = []) { + const invalidLocales = locales.filter( + (locale) => !VALID_LOCALE_FORMATS.some((format) => locale.match(format)), + ); + if (invalidLocales.length) { + throw new FirebaseError( + `Invalid i18n locales (${invalidLocales.join( + ", ", + )}) for Firebase. See our docs for more information https://firebase.google.com/docs/hosting/i18n-rewrites#country-and-language-codes`, + ); + } +} + +export function getFrameworksBuildTarget(purpose: BUILD_TARGET_PURPOSE, validOptions: string[]) { + const frameworksBuild = process.env.FIREBASE_FRAMEWORKS_BUILD_TARGET; + if (frameworksBuild) { + if (!validOptions.includes(frameworksBuild)) { + throw new FirebaseError( + `Invalid value for FIREBASE_FRAMEWORKS_BUILD_TARGET environment variable: ${frameworksBuild}. Valid values are: ${validOptions.join( + ", ", + )}`, + ); + } + return frameworksBuild; + } else if (["test", "deploy"].includes(purpose)) { + return "production"; + } + // TODO handle other language / frameworks environment variables + switch (process.env.NODE_ENV) { + case undefined: + case "development": + return "development"; + case "production": + case "test": + return "production"; + default: + throw new FirebaseError( + `We cannot infer your build target from a non-standard NODE_ENV. Please set the FIREBASE_FRAMEWORKS_BUILD_TARGET environment variable. Valid values are: ${validOptions.join( + ", ", + )}`, + ); + } +} diff --git a/src/frameworks/vite/index.ts b/src/frameworks/vite/index.ts new file mode 100644 index 00000000000..23da43d0f24 --- /dev/null +++ b/src/frameworks/vite/index.ts @@ -0,0 +1,144 @@ +import { execSync } from "child_process"; +import { spawn } from "cross-spawn"; +import { existsSync } from "fs"; +import { copy, pathExists } from "fs-extra"; +import { join } from "path"; +const stripAnsi = require("strip-ansi"); +import { FrameworkType, SupportLevel } from "../interfaces"; +import { promptOnce } from "../../prompt"; +import { + simpleProxy, + warnIfCustomBuildScript, + findDependency, + getNodeModuleBin, + relativeRequire, +} from "../utils"; + +export const name = "Vite"; +export const support = SupportLevel.Experimental; +export const type = FrameworkType.Toolchain; +export const supportedRange = "3 - 5"; + +export const DEFAULT_BUILD_SCRIPT = ["vite build", "tsc && vite build"]; + +export const initViteTemplate = (template: string) => async (setup: any, config: any) => + await init(setup, config, template); + +export async function init(setup: any, config: any, baseTemplate: string = "vanilla") { + const template = await promptOnce({ + type: "list", + default: "JavaScript", + message: "What language would you like to use?", + choices: [ + { name: "JavaScript", value: baseTemplate }, + { name: "TypeScript", value: `${baseTemplate}-ts` }, + ], + }); + execSync( + `npm create vite@"${supportedRange}" ${setup.hosting.source} --yes -- --template ${template}`, + { + stdio: "inherit", + cwd: config.projectDir, + }, + ); + execSync(`npm install`, { stdio: "inherit", cwd: join(config.projectDir, setup.hosting.source) }); +} + +export const viteDiscoverWithNpmDependency = (dep: string) => async (dir: string) => + await discover(dir, undefined, dep); + +export const vitePluginDiscover = (plugin: string) => async (dir: string) => + await discover(dir, plugin); + +export async function discover(dir: string, plugin?: string, npmDependency?: string) { + if (!existsSync(join(dir, "package.json"))) return; + // If we're not searching for a vite plugin, depth has to be zero + const additionalDep = + npmDependency && findDependency(npmDependency, { cwd: dir, depth: 0, omitDev: false }); + const depth = plugin ? undefined : 0; + const configFilesExist = await Promise.all([ + pathExists(join(dir, "vite.config.js")), + pathExists(join(dir, "vite.config.ts")), + ]); + const anyConfigFileExists = configFilesExist.some((it) => it); + const version: string | undefined = findDependency("vite", { + cwd: dir, + depth, + omitDev: false, + })?.version; + if (!anyConfigFileExists && !version) return; + if (npmDependency && !additionalDep) return; + const { appType, publicDir: publicDirectory, plugins } = await getConfig(dir); + if (plugin && !plugins.find(({ name }) => name === plugin)) return; + return { + mayWantBackend: appType !== "spa", + publicDirectory, + version, + vite: true, + }; +} + +export async function build(root: string, target: string) { + const { build } = await relativeRequire(root, "vite"); + + await warnIfCustomBuildScript(root, name, DEFAULT_BUILD_SCRIPT); + + // SvelteKit uses process.cwd() unfortunately, chdir + const cwd = process.cwd(); + process.chdir(root); + + const originalNodeEnv = process.env.NODE_ENV; + + // Downcasting as `string` as otherwise it is inferred as `readonly 'NODE_ENV'`, + // but `env[key]` expects a non-readonly variable. + const envKey: string = "NODE_ENV"; + // Voluntarily making .env[key] not statically analyzable to avoid + // Webpack from converting it to "development" = target; + process.env[envKey] = target; + + await build({ root, mode: target }); + process.chdir(cwd); + + // Voluntarily making .env[key] not statically analyzable to avoid + // Webpack from converting it to "development" = target; + process.env[envKey] = originalNodeEnv; + + return { rewrites: [{ source: "**", destination: "/index.html" }] }; +} + +export async function ɵcodegenPublicDirectory(root: string, dest: string) { + const viteConfig = await getConfig(root); + const viteDistPath = join(root, viteConfig.build.outDir); + await copy(viteDistPath, dest); +} + +export async function getDevModeHandle(dir: string) { + const host = new Promise((resolve, reject) => { + // Can't use scheduleTarget since that—like prerender—is failing on an ESM bug + // will just grep for the hostname + const cli = getNodeModuleBin("vite", dir); + const serve = spawn(cli, [], { cwd: dir }); + serve.stdout.on("data", (data: any) => { + process.stdout.write(data); + const dataWithoutAnsiCodes = stripAnsi(data.toString()); + const match = dataWithoutAnsiCodes.match(/(http:\/\/.+:\d+)/); + if (match) resolve(match[1]); + }); + serve.stderr.on("data", (data: any) => { + process.stderr.write(data); + }); + + serve.on("exit", reject); + }); + return simpleProxy(await host); +} + +async function getConfig(root: string) { + const { resolveConfig } = await relativeRequire(root, "vite"); + // SvelteKit uses process.cwd() unfortunately, we should be defensive here + const cwd = process.cwd(); + process.chdir(root); + const config = await resolveConfig({ root }, "build", "production"); + process.chdir(cwd); + return config; +} diff --git a/src/test/fsAsync.spec.ts b/src/fsAsync.spec.ts similarity index 98% rename from src/test/fsAsync.spec.ts rename to src/fsAsync.spec.ts index d75d7b15d53..56e846edbc6 100644 --- a/src/test/fsAsync.spec.ts +++ b/src/fsAsync.spec.ts @@ -5,7 +5,7 @@ import * as os from "os"; import * as path from "path"; import { sync as rimraf } from "rimraf"; -import * as fsAsync from "../fsAsync"; +import * as fsAsync from "./fsAsync"; // These tests work on the following directory structure: // @@ -46,7 +46,7 @@ describe("fsAsync", () => { rimraf(baseDir); expect(() => { fs.statSync(baseDir); - }).to.throw; + }).to.throw(); }); describe("readdirRecursive", () => { diff --git a/src/fsAsync.ts b/src/fsAsync.ts index fc4256f6dfd..f8a9eee66d3 100644 --- a/src/fsAsync.ts +++ b/src/fsAsync.ts @@ -21,7 +21,7 @@ async function readdirRecursiveHelper(options: { }): Promise { const dirContents = readdirSync(options.path); const fullPaths = dirContents.map((n) => join(options.path, n)); - const filteredPaths = _.reject(fullPaths, options.filter); + const filteredPaths = fullPaths.filter((p) => !options.filter(p)); const filePromises: Array> = []; for (const p of filteredPaths) { const fstat = statSync(p); @@ -36,7 +36,7 @@ async function readdirRecursiveHelper(options: { const files = await Promise.all(filePromises); let flatFiles = _.flattenDeep(files); - flatFiles = _.reject(flatFiles, (f) => _.isNull(f)); + flatFiles = flatFiles.filter((f) => f !== null); return flatFiles; } @@ -46,10 +46,10 @@ async function readdirRecursiveHelper(options: { * @return array of files that match. */ export async function readdirRecursive( - options: ReaddirRecursiveOpts + options: ReaddirRecursiveOpts, ): Promise { const mmopts = { matchBase: true, dot: true }; - const rules = _.map(options.ignore || [], (glob) => { + const rules = (options.ignore || []).map((glob) => { return (p: string) => minimatch(p, glob, mmopts); }); const filter = (t: string): boolean => { diff --git a/src/test/fsutils.spec.ts b/src/fsutils.spec.ts similarity index 96% rename from src/test/fsutils.spec.ts rename to src/fsutils.spec.ts index 3a7eddba71c..121db28fac3 100644 --- a/src/test/fsutils.spec.ts +++ b/src/fsutils.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; -import * as fsutils from "../fsutils"; +import * as fsutils from "./fsutils"; describe("fsutils", () => { describe("fileExistsSync", () => { diff --git a/src/fsutils.ts b/src/fsutils.ts index 98300331461..c1071f22e6f 100644 --- a/src/fsutils.ts +++ b/src/fsutils.ts @@ -1,9 +1,10 @@ -import { statSync } from "fs"; +import { readFileSync, statSync } from "fs"; +import { FirebaseError } from "./error"; export function fileExistsSync(path: string): boolean { try { return statSync(path).isFile(); - } catch (e) { + } catch (e: any) { return false; } } @@ -11,7 +12,18 @@ export function fileExistsSync(path: string): boolean { export function dirExistsSync(path: string): boolean { try { return statSync(path).isDirectory(); - } catch (e) { + } catch (e: any) { return false; } } + +export function readFile(path: string): string { + try { + return readFileSync(path).toString(); + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === "ENOENT") { + throw new FirebaseError(`File not found: ${path}`); + } + throw e; + } +} diff --git a/src/functional.spec.ts b/src/functional.spec.ts new file mode 100644 index 00000000000..574929a9b36 --- /dev/null +++ b/src/functional.spec.ts @@ -0,0 +1,163 @@ +import { expect } from "chai"; +import { flatten } from "lodash"; +import { SameType } from "./metaprogramming"; + +import * as f from "./functional"; + +describe("functional", () => { + describe("flatten", () => { + it("can iterate an empty object", () => { + expect([...f.flatten({})]).to.deep.equal([]); + }); + + it("can iterate an object that's already flat", () => { + expect([...f.flatten({ a: "b" })]).to.deep.equal([["a", "b"]]); + }); + + it("Gets the right type for flattening arrays", () => { + const arr = [[["a"], "b"], ["c"]]; + const flattened = [...f.flattenArray(arr)]; + const test: SameType = true; + expect(test).to.be.true; + }); + + it("can handle nested objects", () => { + const init = { + outer: { + inner: { + value: 42, + }, + }, + other: { + value: null, + }, + }; + + const expected = [ + ["outer.inner.value", 42], + ["other.value", null], + ]; + + expect([...f.flatten(init)]).to.deep.equal(expected); + }); + + it("can handle objects with array values", () => { + const init = { values: ["a", "b"] }; + const expected = [ + ["values.0", "a"], + ["values.1", "b"], + ]; + + expect([...f.flatten(init)]).to.deep.equal(expected); + }); + + it("can iterate an empty array", () => { + expect([...flatten([])]).to.deep.equal([]); + }); + + it("can noop arrays", () => { + const init = ["a", "b", "c"]; + expect([...f.flatten(init)]).to.deep.equal(init); + }); + + it("can flatten", () => { + const init = [[[1]], [2], 3]; + expect([...f.flatten(init)]).to.deep.equal([1, 2, 3]); + }); + }); + + describe("reduceFlat", () => { + it("can noop", () => { + const init = ["a", "b", "c"]; + expect(init.reduce(f.reduceFlat, [])).to.deep.equal(["a", "b", "c"]); + }); + + it("can flatten", () => { + const init = [[[1]], [2], 3]; + expect(init.reduce(f.reduceFlat, [])).to.deep.equal([1, 2, 3]); + }); + }); + + describe("zip", () => { + it("can handle an empty array", () => { + expect([...f.zip([], [])]).to.deep.equal([]); + }); + it("can zip", () => { + expect([...f.zip([1], ["a"])]).to.deep.equal([[1, "a"]]); + }); + it("throws on length mismatch", () => { + expect(() => [...f.zip([1], [])]).to.throw(); + }); + }); + + it("zipIn", () => { + expect([1, 2].map(f.zipIn(["a", "b"]))).to.deep.equal([ + [1, "a"], + [2, "b"], + ]); + }); + + it("assertExhaustive", () => { + interface Bird { + type: "bird"; + } + interface Fish { + type: "fish"; + } + type Animal = Bird | Fish; + + // eslint-disable-next-line + function passtime(animal: Animal): string { + if (animal.type === "bird") { + return "fly"; + } else if (animal.type === "fish") { + return "swim"; + } + + // This line must make the containing function compile: + f.assertExhaustive(animal); + } + + // eslint-disable-next-line + function speak(animal: Animal): void { + if (animal.type === "bird") { + console.log("chirp"); + return; + } + // This line must cause the containing function to fail + // compilation if uncommented + // f.assertExhaustive(animal); + } + }); + + describe("partition", () => { + it("should split an array into true and false", () => { + const arr = ["T1", "F1", "T2", "F2"]; + expect(f.partition(arr, (s: string) => s.startsWith("T"))).to.deep.equal([ + ["T1", "T2"], + ["F1", "F2"], + ]); + }); + + it("can handle an empty array", () => { + expect(f.partition([], (s: string) => s.startsWith("T"))).to.deep.equal([[], []]); + }); + }); + + describe("partitionRecord", () => { + it("should split a record into true and false", () => { + const rec = { T1: 1, F1: 2, T2: 3, F2: 4 }; + expect(f.partitionRecord(rec, (s: string) => s.startsWith("T"))).to.deep.equal([ + { T1: 1, T2: 3 }, + { F1: 2, F2: 4 }, + ]); + }); + + it("can handle an empty record", () => { + expect(f.partitionRecord({}, (s: string) => s.startsWith("T"))).to.deep.equal([ + {}, + {}, + ]); + }); + }); +}); diff --git a/src/functional.ts b/src/functional.ts index db0135f0fc3..f68f2d48ae3 100644 --- a/src/functional.ts +++ b/src/functional.ts @@ -1,3 +1,5 @@ +import { LeafElems } from "./metaprogramming"; + /** * Flattens an object so that the return value's keys are the path * to a value in the source object. E.g. flattenObject({the: {answer: 42}}) @@ -5,14 +7,14 @@ * @param obj An object to be flattened * @return An array where values come from obj and keys are the path in obj to that value. */ -export function* flattenObject(obj: Record): Generator<[string, unknown]> { - function* helper(path: string[], obj: Record): Generator<[string, unknown]> { +export function* flattenObject(obj: T): Generator<[string, unknown]> { + function* helper(path: string[], obj: V): Generator<[string, unknown]> { for (const [k, v] of Object.entries(obj)) { if (typeof v !== "object" || v === null) { yield [[...path, k].join("."), v]; } else { // Object.entries loses type info, so we must cast - yield* helper([...path, k], v as Record); + yield* helper([...path, k], v); } } } @@ -25,28 +27,26 @@ export function* flattenObject(obj: Record): Generator<[string, * [...flatten([[[1]], [2], 3])] = [1, 2, 3] */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function* flattenArray(arr: unknown[]): Generator { +export function* flattenArray(arr: T): Generator> { for (const val of arr) { if (Array.isArray(val)) { yield* flattenArray(val); } else { - yield val as T; + yield val as LeafElems; } } } /** Shorthand for flattenObject. */ -export function flatten(obj: Record): Generator<[string, unknown]>; +export function flatten(obj: T): Generator<[string, string]>; /** Shorthand for flattenArray. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function flatten(arr: unknown[]): Generator; +export function flatten(arr: T): Generator>; /** Flattens an object or array. */ -export function flatten( - objOrArr: Record | unknown[] -): Generator<[string, unknown]> | Generator { +export function flatten(objOrArr: T): unknown { if (Array.isArray(objOrArr)) { - return flattenArray(objOrArr); + return flattenArray(objOrArr); } else { return flattenObject(objOrArr); } @@ -59,7 +59,7 @@ export function flatten( */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function reduceFlat(accum: T[] | undefined, next: unknown): T[] { - return [...(accum || []), ...flatten([next])]; + return [...(accum || []), ...(flatten([next]) as Generator)]; } /** @@ -67,7 +67,7 @@ export function reduceFlat(accum: T[] | undefined, next: unknown): T[] * [...zip([1, 2, 3], ['a', 'b', 'c'])] = [[1, 'a], [2, 'b'], [3, 'c']] */ export function* zip(left: T[], right: V[]): Generator<[T, V]> { - if (left.length != right.length) { + if (left.length !== right.length) { throw new Error("Cannot zip between two lists of differen lengths"); } for (let i = 0; i < left.length; i++) { @@ -79,27 +79,70 @@ export function* zip(left: T[], right: V[]): Generator<[T, V]> { * Utility to zip in another array from map. * [1, 2].map(zipIn(['a', 'b'])) = [[1, 'a'], [2, 'b']] */ -export const zipIn = (other: V[]) => (elem: T, ndx: number): [T, V] => { - return [elem, other[ndx]]; -}; +export const zipIn = + (other: V[]) => + (elem: T, ndx: number): [T, V] => { + return [elem, other[ndx]]; + }; /** Used with type guards to guarantee that all cases have been covered. */ -export function assertExhaustive(val: never): never { +export function assertExhaustive(val: never, message?: string): never { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Never has a value (${val}). This should be impossible`); + throw new Error(message || `Never has a value (${val}).`); } /** - * Utility to partition an array into two based on callbackFn's truthiness for each element. + * Utility to partition an array into two based on predicate's truthiness for each element. * Returns a Array containing two Array. The first array contains all elements that returned true, * the second contains all elements that returned false. */ -export function partition(arr: T[], callbackFn: (elem: T) => boolean): T[][] { - return arr.reduce( +export function partition(arr: T[], predicate: (elem: T) => boolean): [T[], T[]] { + return arr.reduce<[T[], T[]]>( (acc, elem) => { - acc[callbackFn(elem) ? 0 : 1].push(elem); + acc[predicate(elem) ? 0 : 1].push(elem); + return acc; + }, + [[], []], + ); +} + +/** + * Utility to partition a Record into two based on predicate's truthiness for each element. + * Returns a Array containing two Record. The first array contains all elements that returned true, + * the second contains all elements that returned false. + */ +export function partitionRecord( + rec: Record, + predicate: (key: string, val: T) => boolean, +): [Record, Record] { + return Object.entries(rec).reduce<[Record, Record]>( + (acc, [key, val]) => { + acc[predicate(key, val) ? 0 : 1][key] = val; return acc; }, - [[], []] + [{}, {}], ); } + +/** + * Create a map of transformed values for all keys. + */ +export function mapObject( + input: Record, + transform: (t: T) => V, +): Record { + const result: Record = {}; + for (const [k, v] of Object.entries(input)) { + result[k] = transform(v); + } + return result; +} + +export const nullsafeVisitor = + (func: (first: First, ...rest: Rest) => Ret, ...rest: Rest) => + (first: First | null): Ret | null => { + if (first === null) { + return null; + } + return func(first, ...rest); + }; diff --git a/src/functions/constants.ts b/src/functions/constants.ts new file mode 100644 index 00000000000..99bd2bf716b --- /dev/null +++ b/src/functions/constants.ts @@ -0,0 +1,13 @@ +import { AUTH_BLOCKING_EVENTS } from "./events/v1"; + +export const CODEBASE_LABEL = "firebase-functions-codebase"; +export const HASH_LABEL = "firebase-functions-hash"; +export const BLOCKING_LABEL = "deployment-blocking"; +export const BLOCKING_LABEL_KEY_TO_EVENT: Record = { + "before-create": "providers/cloud.auth/eventTypes/user.beforeCreate", + "before-sign-in": "providers/cloud.auth/eventTypes/user.beforeSignIn", +}; +export const BLOCKING_EVENT_TO_LABEL_KEY: Record<(typeof AUTH_BLOCKING_EVENTS)[number], string> = { + "providers/cloud.auth/eventTypes/user.beforeCreate": "before-create", + "providers/cloud.auth/eventTypes/user.beforeSignIn": "before-sign-in", +}; diff --git a/src/functions/ensureTargeted.spec.ts b/src/functions/ensureTargeted.spec.ts new file mode 100644 index 00000000000..e8f697e5fa6 --- /dev/null +++ b/src/functions/ensureTargeted.spec.ts @@ -0,0 +1,32 @@ +import { expect } from "chai"; +import { ensureTargeted } from "./ensureTargeted"; + +describe("ensureTargeted", () => { + it("does nothing if 'functions' is included", () => { + expect(ensureTargeted("hosting,functions", "codebase")).to.equal("hosting,functions"); + expect(ensureTargeted("hosting,functions", "codebase", "id")).to.equal("hosting,functions"); + }); + + it("does nothing if the codebase is targeted", () => { + expect(ensureTargeted("hosting,functions:codebase", "codebase")).to.equal( + "hosting,functions:codebase", + ); + expect(ensureTargeted("hosting,functions:codebase", "codebase", "id")).to.equal( + "hosting,functions:codebase", + ); + }); + + it("does nothing if the function is targeted", () => { + expect(ensureTargeted("hosting,functions:codebase:id", "codebase", "id")).to.equal( + "hosting,functions:codebase:id", + ); + }); + + it("adds the codebase if missing and no id is provided", () => { + expect(ensureTargeted("hosting", "codebase")).to.equal("hosting,functions:codebase"); + }); + + it("adds the function if missing", () => { + expect(ensureTargeted("hosting", "codebase", "id")).to.equal("hosting,functions:codebase:id"); + }); +}); diff --git a/src/functions/ensureTargeted.ts b/src/functions/ensureTargeted.ts new file mode 100644 index 00000000000..81afaef0ecf --- /dev/null +++ b/src/functions/ensureTargeted.ts @@ -0,0 +1,49 @@ +/** + * Ensures than an only string is modified so that it will enclude a function + * in its target. This is useful for making sure that an SSR function is included + * with a web framework, or that a traditional hosting site includes its pinned + * functions + * @param only original only string + * @param codebaseOrFunction codebase or function ID + * @return new only string + */ +export function ensureTargeted(only: string, codebaseOrFunction: string): string; + +/** + * Ensures than an only string is modified so that it will enclude a function + * in its target. This is useful for making sure that an SSR function is included + * with a web framework, or that a traditional hosting site includes its pinned + * functions + * @param only original only string + * @param codebase codebase id + * @param functionId function id + * @return new only string + */ +export function ensureTargeted(only: string, codebase: string, functionId: string): string; + +/** + * Implementation of ensureTargeted. + */ +export function ensureTargeted( + only: string, + codebaseOrFunction: string, + functionId?: string, +): string { + const parts = only.split(","); + if (parts.includes("functions")) { + return only; + } + + let newTarget = `functions:${codebaseOrFunction}`; + if (parts.includes(newTarget)) { + return only; + } + if (functionId) { + newTarget = `${newTarget}:${functionId}`; + if (parts.includes(newTarget)) { + return only; + } + } + + return `${only},${newTarget}`; +} diff --git a/src/functions/env.spec.ts b/src/functions/env.spec.ts new file mode 100644 index 00000000000..1b4e0a88a5f --- /dev/null +++ b/src/functions/env.spec.ts @@ -0,0 +1,699 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { sync as rimraf } from "rimraf"; +import { expect } from "chai"; + +import * as env from "./env"; +import { FirebaseError } from "../error"; + +describe("functions/env", () => { + describe("parse", () => { + const tests: { description: string; input: string; want: Record }[] = [ + { + description: "should parse values with trailing spaces", + input: "FOO=foo ", + want: { FOO: "foo" }, + }, + { + description: "should parse exported values", + input: "export FOO=foo", + want: { FOO: "foo" }, + }, + { + description: "should parse values with trailing spaces (single quotes)", + input: "FOO='foo' ", + want: { FOO: "foo" }, + }, + { + description: "should parse values with trailing spaces (double quotes)", + input: 'FOO="foo" ', + want: { FOO: "foo" }, + }, + { + description: "should parse double quoted, multi-line values", + input: ` +FOO="foo1 +foo2" +BAR=bar +`, + want: { FOO: "foo1\nfoo2", BAR: "bar" }, + }, + { + description: "should parse many double quoted values", + input: 'FOO="foo"\nBAR="bar"', + want: { FOO: "foo", BAR: "bar" }, + }, + { + description: "should parse many single quoted values", + input: "FOO='foo'\nBAR='bar'", + want: { FOO: "foo", BAR: "bar" }, + }, + { + description: "should parse mix of double and single quoted values", + input: `FOO="foo"\nBAR='bar'`, + want: { FOO: "foo", BAR: "bar" }, + }, + { + description: "should parse double quoted with escaped newlines", + input: 'FOO="foo1\\nfoo2"\nBAR=bar', + want: { FOO: "foo1\nfoo2", BAR: "bar" }, + }, + { + description: "should parse escape sequences in order, from start to end", + input: `BAZ=baz +ONE_NEWLINE="foo1\\nfoo2" +ONE_BSLASH_AND_N="foo3\\\\nfoo4" +ONE_BSLASH_AND_NEWLINE="foo5\\\\\\nfoo6" +TWO_BSLASHES_AND_N="foo7\\\\\\\\nfoo8" +BAR=bar`, + want: { + BAZ: "baz", + ONE_NEWLINE: "foo1\nfoo2", + ONE_BSLASH_AND_N: "foo3\\nfoo4", + ONE_BSLASH_AND_NEWLINE: "foo5\\\nfoo6", + TWO_BSLASHES_AND_N: "foo7\\\\nfoo8", + BAR: "bar", + }, + }, + { + description: "should parse double quoted with multiple escaped newlines", + input: 'FOO="foo1\\nfoo2\\nfoo3"\nBAR=bar', + want: { FOO: "foo1\nfoo2\nfoo3", BAR: "bar" }, + }, + { + description: "should parse double quoted with multiple escaped horizontal tabs", + input: 'FOO="foo1\\tfoo2\\tfoo3"\nBAR=bar', + want: { FOO: "foo1\tfoo2\tfoo3", BAR: "bar" }, + }, + { + description: "should parse double quoted with multiple escaped vertical tabs", + input: 'FOO="foo1\\vfoo2\\vfoo3"\nBAR=bar', + want: { FOO: "foo1\vfoo2\vfoo3", BAR: "bar" }, + }, + { + description: "should parse double quoted with multiple escaped carriage returns", + input: 'FOO="foo1\\rfoo2\\rfoo3"\nBAR=bar', + want: { FOO: "foo1\rfoo2\rfoo3", BAR: "bar" }, + }, + { + description: "should leave single quotes when double quoted", + input: `FOO="'foo'"`, + want: { FOO: "'foo'" }, + }, + { + description: "should leave double quotes when single quoted", + input: `FOO='"foo"'`, + want: { FOO: '"foo"' }, + }, + { + description: "should unescape escape characters for double quoted values", + input: 'FOO="foo1\\"foo2"', + want: { FOO: 'foo1"foo2' }, + }, + { + description: "should leave escape characters intact for single quoted values", + input: "FOO='foo1\\'foo2'", + want: { FOO: "foo1\\'foo2" }, + }, + { + description: "should leave escape characters intact for unquoted values", + input: "FOO=foo1\\'foo2", + want: { FOO: "foo1\\'foo2" }, + }, + { + description: "should parse empty value", + input: "FOO=", + want: { FOO: "" }, + }, + { + description: "should parse keys with leading spaces", + input: " FOO=foo ", + want: { FOO: "foo" }, + }, + { + description: "should parse values with trailing spaces (unquoted)", + input: "FOO=foo ", + want: { FOO: "foo" }, + }, + { + description: "should parse values with trailing spaces (single quoted)", + input: "FOO='foo ' ", + want: { FOO: "foo " }, + }, + { + description: "should parse values with trailing spaces (double quoted)", + input: 'FOO="foo " ', + want: { FOO: "foo " }, + }, + { + description: "should throw away unquoted values following #", + input: "FOO=foo#bar", + want: { FOO: "foo" }, + }, + { + description: "should keep values following # in singqle quotes", + input: "FOO='foo#bar'", + want: { FOO: "foo#bar" }, + }, + { + description: "should keep values following # in double quotes", + input: 'FOO="foo#bar"', + want: { FOO: "foo#bar" }, + }, + { + description: "should ignore leading/trailing spaces before the separator (unquoted)", + input: "FOO = foo", + want: { FOO: "foo" }, + }, + { + description: "should ignore leading/trailing spaces before the separator (single quotes)", + input: "FOO = 'foo'", + want: { FOO: "foo" }, + }, + { + description: "should ignore leading/trailing spaces before the separator (double quotes)", + input: 'FOO = "foo"', + want: { FOO: "foo" }, + }, + { + description: "should handle empty values", + input: ` +FOO= +BAR= "blah" +`, + want: { FOO: "", BAR: "blah" }, + }, + { + description: "should handle quoted values after a newline", + input: ` +FOO= +"blah" +`, + want: { FOO: "blah" }, + }, + { + description: "should ignore comments", + input: ` + FOO=foo # comment + # line comment 1 + # line comment 2 + BAR=bar # another comment + `, + want: { FOO: "foo", BAR: "bar" }, + }, + { + description: "should ignore empty lines", + input: ` + FOO=foo + + BAR=bar + + `, + want: { FOO: "foo", BAR: "bar" }, + }, + ]; + + tests.forEach(({ description, input, want }) => { + it(description, () => { + const { envs, errors } = env.parse(input); + expect(envs).to.deep.equal(want); + expect(errors).to.be.empty; + }); + }); + + it("should catch invalid lines", () => { + expect( + env.parse(` +BAR### +FOO=foo +// not a comment +=missing key +`), + ).to.deep.equal({ + envs: { FOO: "foo" }, + errors: ["BAR###", "// not a comment", "=missing key"], + }); + }); + }); + + describe("validateKey", () => { + it("accepts valid keys", () => { + const keys = ["FOO", "ABC_EFG", "A1_B2"]; + keys.forEach((key) => { + expect(() => { + env.validateKey(key); + }).not.to.throw(); + }); + }); + + it("throws error given invalid keys", () => { + const keys = ["", "1F", "B=C"]; + keys.forEach((key) => { + expect(() => { + env.validateKey(key); + }).to.throw("must start with"); + }); + }); + + it("throws error given reserved keys", () => { + const keys = [ + "FIREBASE_CONFIG", + "FUNCTION_TARGET", + "FUNCTION_SIGNATURE_TYPE", + "K_SERVICE", + "K_REVISION", + "PORT", + "K_CONFIGURATION", + ]; + keys.forEach((key) => { + expect(() => { + env.validateKey(key); + }).to.throw("reserved for internal use"); + }); + }); + + it("throws error given keys with a reserved prefix", () => { + expect(() => { + env.validateKey("X_GOOGLE_FOOBAR"); + }).to.throw("starts with a reserved prefix"); + + expect(() => { + env.validateKey("FIREBASE_FOOBAR"); + }).to.throw("starts with a reserved prefix"); + + expect(() => { + env.validateKey("EXT_INSTANCE_ID"); + }).to.throw("starts with a reserved prefix"); + }); + }); + + describe("writeUserEnvs", () => { + const createEnvFiles = (sourceDir: string, envs: Record): void => { + for (const [filename, data] of Object.entries(envs)) { + fs.writeFileSync(path.join(sourceDir, filename), data); + } + }; + let tmpdir: string; + + beforeEach(() => { + tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test")); + }); + + afterEach(() => { + rimraf(tmpdir); + expect(() => { + fs.statSync(tmpdir); + }).to.throw(); + }); + + it("never affects the filesystem if the list of keys to write is empty", () => { + env.writeUserEnvs( + {}, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ); + env.writeUserEnvs( + {}, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true }, + ); + expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw; + expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw; + expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw; + }); + + it("touches .env.projectId if it doesn't already exist", () => { + env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir }); + expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw; + expect(!!fs.statSync(path.join(tmpdir, ".env.project"))).to.be.true; + expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw; + }); + + it("touches .env.local if it doesn't already exist in emulator mode", () => { + env.writeUserEnvs( + { FOO: "bar" }, + { projectId: "project", functionsSource: tmpdir, isEmulator: true }, + ); + expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw; + expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw; + expect(!!fs.statSync(path.join(tmpdir, ".env.local"))).to.be.true; + }); + + it("throws if asked to write a key that already exists in .env.projectId", () => { + createEnvFiles(tmpdir, { + [".env.project"]: "FOO=foo", + }); + expect(() => + env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir }), + ).to.throw(FirebaseError); + }); + + it("is fine writing a key that already exists in .env.projectId but not .env.local, in emulator mode", () => { + createEnvFiles(tmpdir, { + [".env.project"]: "FOO=foo", + }); + env.writeUserEnvs( + { FOO: "bar" }, + { projectId: "project", functionsSource: tmpdir, isEmulator: true }, + ); + expect( + env.loadUserEnvs({ + projectId: "project", + projectAlias: "alias", + functionsSource: tmpdir, + isEmulator: true, + })["FOO"], + ).to.equal("bar"); + }); + + it("throws if asked to write a key that already exists in any .env", () => { + createEnvFiles(tmpdir, { + [".env"]: "FOO=bar", + }); + expect(() => + env.writeUserEnvs( + { FOO: "baz" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ), + ).to.throw(FirebaseError); + }); + + it("is fine writing a key that already exists in any .env but not .env.local, in emulator mode", () => { + createEnvFiles(tmpdir, { + [".env"]: "FOO=bar", + }); + env.writeUserEnvs( + { FOO: "baz" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true }, + ); + expect( + env.loadUserEnvs({ + projectId: "project", + projectAlias: "alias", + functionsSource: tmpdir, + isEmulator: true, + })["FOO"], + ).to.equal("baz"); + }); + + it("throws if asked to write a key that already exists in .env.local, in emulator mode", () => { + createEnvFiles(tmpdir, { + [".env.local"]: "ASDF=foo", + }); + expect(() => + env.writeUserEnvs( + { ASDF: "bar" }, + { projectId: "project", functionsSource: tmpdir, isEmulator: true }, + ), + ).to.throw(FirebaseError); + }); + + it("throws if asked to write a key that fails key format validation", () => { + expect(() => + env.writeUserEnvs( + { lowercase: "bar" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ), + ).to.throw(env.KeyValidationError); + expect(() => + env.writeUserEnvs( + { GCP_PROJECT: "bar" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ), + ).to.throw(env.KeyValidationError); + expect(() => + env.writeUserEnvs( + { FIREBASE_KEY: "bar" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ), + ).to.throw(env.KeyValidationError); + }); + + it("writes the specified key to a .env.projectId that it created", () => { + env.writeUserEnvs( + { FOO: "bar" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ); + expect( + env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir })[ + "FOO" + ], + ).to.equal("bar"); + }); + + it("writes the specified key to a .env.projectId that already existed", () => { + createEnvFiles(tmpdir, { + [".env.project"]: "", + }); + env.writeUserEnvs( + { FOO: "bar" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ); + expect( + env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir })[ + "FOO" + ], + ).to.equal("bar"); + }); + + it("writes multiple keys at once", () => { + env.writeUserEnvs( + { FOO: "foo", BAR: "bar" }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ); + const envs = env.loadUserEnvs({ + projectId: "project", + projectAlias: "alias", + functionsSource: tmpdir, + }); + expect(envs["FOO"]).to.equal("foo"); + expect(envs["BAR"]).to.equal("bar"); + }); + + it("escapes special characters so that parse() can reverse them", () => { + env.writeUserEnvs( + { + ESCAPES: "\n\r\t\v", + WITH_SLASHES: "\n\\\r\\\t\\\v", + QUOTES: "'\"'", + }, + { projectId: "project", projectAlias: "alias", functionsSource: tmpdir }, + ); + const envs = env.loadUserEnvs({ + projectId: "project", + projectAlias: "alias", + functionsSource: tmpdir, + }); + expect(envs["ESCAPES"]).to.equal("\n\r\t\v"); + expect(envs["WITH_SLASHES"]).to.equal("\n\\\r\\\t\\\v"); + expect(envs["QUOTES"]).to.equal("'\"'"); + }); + + it("shouldn't write anything if any of the keys fails key format validation", () => { + try { + env.writeUserEnvs( + { FOO: "bar", lowercase: "bar" }, + { projectId: "project", functionsSource: tmpdir }, + ); + } catch (err: any) { + // no-op + } + expect(env.loadUserEnvs({ projectId: "project", functionsSource: tmpdir })["FOO"]).to.be + .undefined; + }); + }); + + describe("loadUserEnvs", () => { + const createEnvFiles = (sourceDir: string, envs: Record): void => { + for (const [filename, data] of Object.entries(envs)) { + fs.writeFileSync(path.join(sourceDir, filename), data); + } + }; + const projectInfo: Omit = { + projectId: "my-project", + projectAlias: "dev", + }; + let tmpdir: string; + + beforeEach(() => { + tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test")); + }); + + afterEach(() => { + rimraf(tmpdir); + expect(() => { + fs.statSync(tmpdir); + }).to.throw(); + }); + + it("loads nothing if .env files are missing", () => { + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({}); + }); + + it("loads envs from .env file", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=foo\nBAR=bar", + }); + + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ + FOO: "foo", + BAR: "bar", + }); + }); + + it("loads envs from .env file, ignoring comments", () => { + createEnvFiles(tmpdir, { + ".env": "# THIS IS A COMMENT\nFOO=foo # inline comments\nBAR=bar", + }); + + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ + FOO: "foo", + BAR: "bar", + }); + }); + + it("loads envs from .env. file", () => { + createEnvFiles(tmpdir, { + [`.env.${projectInfo.projectId}`]: "FOO=foo\nBAR=bar", + }); + + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ + FOO: "foo", + BAR: "bar", + }); + }); + + it("loads envs from .env. file", () => { + createEnvFiles(tmpdir, { + [`.env.${projectInfo.projectAlias}`]: "FOO=foo\nBAR=bar", + }); + + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ + FOO: "foo", + BAR: "bar", + }); + }); + + it("loads envs, preferring ones from .env.", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=bad\nBAR=bar", + [`.env.${projectInfo.projectId}`]: "FOO=good", + }); + + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ + FOO: "good", + BAR: "bar", + }); + }); + + it("loads envs, preferring ones from .env. for emulators too", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=bad\nBAR=bar", + [`.env.${projectInfo.projectId}`]: "FOO=good", + }); + + expect( + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir, isEmulator: true }), + ).to.be.deep.equal({ + FOO: "good", + BAR: "bar", + }); + }); + + it("loads envs, preferring ones from .env.", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=bad\nBAR=bar", + [`.env.${projectInfo.projectAlias}`]: "FOO=good", + }); + + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ + FOO: "good", + BAR: "bar", + }); + }); + + it("loads envs, preferring ones from .env. for emulators too", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=bad\nBAR=bar", + [`.env.${projectInfo.projectAlias}`]: "FOO=good", + }); + + expect( + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir, isEmulator: true }), + ).to.be.deep.equal({ + FOO: "good", + BAR: "bar", + }); + }); + + it("loads envs ignoring .env.local", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=bad\nBAR=bar", + [`.env.${projectInfo.projectId}`]: "FOO=good", + ".env.local": "FOO=bad", + }); + + expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ + FOO: "good", + BAR: "bar", + }); + }); + + it("loads envs, preferring .env.local for the emulator", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=bad\nBAR=bar", + [`.env.${projectInfo.projectId}`]: "FOO=another bad", + ".env.local": "FOO=good", + }); + + expect( + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir, isEmulator: true }), + ).to.be.deep.equal({ + FOO: "good", + BAR: "bar", + }); + }); + + it("throws an error if both .env. and .env. exists", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=foo\nBAR=bar", + [`.env.${projectInfo.projectId}`]: "FOO=not-foo", + [`.env.${projectInfo.projectAlias}`]: "FOO=not-foo", + }); + + expect(() => { + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); + }).to.throw("Can't have both"); + }); + + it("throws an error .env file is invalid", () => { + createEnvFiles(tmpdir, { + ".env": "BAH: foo", + }); + + expect(() => { + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); + }).to.throw("Failed to load"); + }); + + it("throws an error .env file contains invalid keys", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=foo", + [`.env.${projectInfo.projectId}`]: "Foo=bad-key", + }); + + expect(() => { + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); + }).to.throw("Failed to load"); + }); + + it("throws an error .env file contains reserved keys", () => { + createEnvFiles(tmpdir, { + ".env": "FOO=foo\nPORT=100", + }); + + expect(() => { + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); + }).to.throw("Failed to load"); + }); + }); +}); diff --git a/src/functions/env.ts b/src/functions/env.ts index 644cd24be8f..d00b2056735 100644 --- a/src/functions/env.ts +++ b/src/functions/env.ts @@ -1,18 +1,19 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import * as path from "path"; import { FirebaseError } from "../error"; import { logger } from "../logger"; -import { previews } from "../previews"; -import { logBullet } from "../utils"; +import { logBullet, logWarning } from "../utils"; const FUNCTIONS_EMULATOR_DOTENV = ".env.local"; +const RESERVED_PREFIXES = ["X_GOOGLE_", "FIREBASE_", "EXT_"]; const RESERVED_KEYS = [ // Cloud Functions for Firebase "FIREBASE_CONFIG", "CLOUD_RUNTIME_CONFIG", + "EVENTARC_CLOUD_EVENT_SOURCE", // Cloud Functions - old runtimes: // https://cloud.google.com/functions/docs/env-var#nodejs_8_python_37_and_go_111 "ENTRY_POINT", @@ -44,7 +45,9 @@ const RESERVED_KEYS = [ const LINE_RE = new RegExp( "^" + // begin line "\\s*" + // leading whitespaces - "(\\w+)" + // key + "(?:export)?" + // Optional 'export' in a non-capture group + "\\s*" + // more whitespaces + "([\\w./]+)" + // key "\\s*=[\\f\\t\\v]*" + // separator (=) "(" + // begin optional value "\\s*'(?:\\\\'|[^'])*'|" + // single quoted or @@ -57,6 +60,28 @@ const LINE_RE = new RegExp( "gms" // flags: global, multiline, dotall ); +const ESCAPE_SEQUENCES_TO_CHARACTERS: Record = { + "\\n": "\n", + "\\r": "\r", + "\\t": "\t", + "\\v": "\v", + "\\\\": "\\", + "\\'": "'", + '\\"': '"', +}; +const ALL_ESCAPE_SEQUENCES_RE = /\\[nrtv\\'"]/g; + +const CHARACTERS_TO_ESCAPE_SEQUENCES: Record = { + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\v": "\\v", + "\\": "\\\\", + "'": "\\'", + '"': '\\"', +}; +const ALL_ESCAPABLE_CHARACTERS_RE = /[\n\r\t\v\\'"]/g; + interface ParseResult { envs: Record; errors: string[]; @@ -103,10 +128,9 @@ export function parse(data: string): ParseResult { // Remove surrounding single/double quotes. v = quotesMatch[2]; if (quotesMatch[1] === '"') { - // Unescape newlines and tabs. - v = v.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\v", "\v"); - // Unescape other escapable characters. - v = v.replace(/\\([\\'"])/g, "$1"); + // Substitute escape sequences. The regex passed to replace() must + // match every key in ESCAPE_SEQUENCES_TO_CHARACTERS. + v = v.replace(ALL_ESCAPE_SEQUENCES_RE, (match) => ESCAPE_SEQUENCES_TO_CHARACTERS[match]); } } @@ -127,7 +151,10 @@ export function parse(data: string): ParseResult { } export class KeyValidationError extends Error { - constructor(public key: string, public message: string) { + constructor( + public key: string, + public message: string, + ) { super(`Failed to validate key ${key}: ${message}`); } } @@ -146,21 +173,23 @@ export function validateKey(key: string): void { throw new KeyValidationError( key, `Key ${key} must start with an uppercase ASCII letter or underscore` + - ", and then consist of uppercase ASCII letters, digits, and underscores." + ", and then consist of uppercase ASCII letters, digits, and underscores.", ); } - if (key.startsWith("X_GOOGLE_") || key.startsWith("FIREBASE_")) { + if (RESERVED_PREFIXES.some((prefix) => key.startsWith(prefix))) { throw new KeyValidationError( key, - `Key ${key} starts with a reserved prefix (X_GOOGLE_ or FIREBASE_)` + `Key ${key} starts with a reserved prefix (${RESERVED_PREFIXES.join(" ")})`, ); } } -// Parse dotenv file, but throw errors if: -// 1. Input has any invalid lines. -// 2. Any env key fails validation. -function parseStrict(data: string): Record { +/** + * Parse dotenv file, but throw errors if: + * 1. Input has any invalid lines. + * 2. Any env key fails validation. + */ +export function parseStrict(data: string): Record { const { envs, errors } = parse(data); if (errors.length) { @@ -171,7 +200,7 @@ function parseStrict(data: string): Record { for (const key of Object.keys(envs)) { try { validateKey(key); - } catch (err) { + } catch (err: any) { logger.debug(`Failed to validate key ${key}: ${err}`); if (err instanceof KeyValidationError) { validationErrors.push(err); @@ -192,16 +221,15 @@ function findEnvfiles( functionsSource: string, projectId: string, projectAlias?: string, - isEmulator?: boolean + isEmulator?: boolean, ): string[] { const files: string[] = [".env"]; + files.push(`.env.${projectId}`); + if (projectAlias) { + files.push(`.env.${projectAlias}`); + } if (isEmulator) { files.push(FUNCTIONS_EMULATOR_DOTENV); - } else { - files.push(`.env.${projectId}`); - if (projectAlias && projectAlias.length) { - files.push(`.env.${projectAlias}`); - } } return files @@ -231,6 +259,95 @@ export function hasUserEnvs({ return findEnvfiles(functionsSource, projectId, projectAlias, isEmulator).length > 0; } +/** + * Write new environment variables into a dotenv file. + * + * Identifies one and only one dotenv file to touch using the same rules as loadUserEnvs(). + * It is an error to provide a key-value pair which is already in the file. + */ +export function writeUserEnvs(toWrite: Record, envOpts: UserEnvsOpts) { + if (Object.keys(toWrite).length === 0) { + return; + } + const { functionsSource, projectId, projectAlias, isEmulator } = envOpts; + + // Determine which .env file to write to, and create it if it doesn't exist + const allEnvFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator); + const targetEnvFile = envOpts.isEmulator + ? FUNCTIONS_EMULATOR_DOTENV + : `.env.${envOpts.projectId}`; + const targetEnvFileExists = allEnvFiles.includes(targetEnvFile); + if (!targetEnvFileExists) { + fs.writeFileSync(path.join(envOpts.functionsSource, targetEnvFile), "", { flag: "wx" }); + logBullet( + clc.yellow(clc.bold("functions: ")) + + `Created new local file ${targetEnvFile} to store param values. We suggest explicitly adding or excluding this file from version control.`, + ); + } + + // Throw if any of the keys are duplicate (note special case if emulator) or malformed + const fullEnvs = loadUserEnvs(envOpts); + const prodEnvs = isEmulator + ? loadUserEnvs({ ...envOpts, isEmulator: false }) + : loadUserEnvs(envOpts); + checkForDuplicateKeys(isEmulator || false, Object.keys(toWrite), fullEnvs, prodEnvs); + for (const k of Object.keys(toWrite)) { + validateKey(k); + } + + // Write all the keys in a single filesystem access + logBullet( + clc.cyan(clc.bold("functions: ")) + `Writing new parameter values to disk: ${targetEnvFile}`, + ); + let lines = ""; + for (const k of Object.keys(toWrite)) { + lines += formatUserEnvForWrite(k, toWrite[k]); + } + fs.appendFileSync(path.join(functionsSource, targetEnvFile), lines); +} + +/** + * Errors if any of the provided keys are aleady defined in the .env fields. + * This seems like a simple presence check, but... + * + * For emulator deploys, it's legal to write a key to .env.local even if it's + * already defined in .env.projectId. This is a special case designed to follow + * the principle of least surprise for emulator users. + */ +export function checkForDuplicateKeys( + isEmulator: boolean, + keys: string[], + fullEnv: Record, + envsWithoutLocal?: Record, +): void { + for (const key of keys) { + const definedInEnv = fullEnv.hasOwnProperty(key); + if (definedInEnv) { + if (envsWithoutLocal && isEmulator && envsWithoutLocal.hasOwnProperty(key)) { + logWarning( + clc.cyan(clc.yellow("functions: ")) + + `Writing parameter ${key} to emulator-specific config .env.local. This will overwrite your existing definition only when emulating.`, + ); + continue; + } + throw new FirebaseError( + `Attempted to write param-defined key ${key} to .env files, but it was already defined.`, + ); + } + } +} + +function formatUserEnvForWrite(key: string, value: string): string { + const escapedValue = value.replace( + ALL_ESCAPABLE_CHARACTERS_RE, + (match) => CHARACTERS_TO_ESCAPE_SEQUENCES[match], + ); + if (escapedValue !== value) { + return `${key}="${escapedValue}"\n`; + } + return `${key}=${escapedValue}\n`; +} + /** * Load user-specified environment variables. * @@ -252,12 +369,8 @@ export function loadUserEnvs({ projectAlias, isEmulator, }: UserEnvsOpts): Record { - if (!previews.dotenv) { - return {}; - } - const envFiles = findEnvfiles(functionsSource, projectId, projectAlias, isEmulator); - if (envFiles.length == 0) { + if (envFiles.length === 0) { return {}; } @@ -266,7 +379,7 @@ export function loadUserEnvs({ if (envFiles.includes(`.env.${projectId}`) && envFiles.includes(`.env.${projectAlias}`)) { throw new FirebaseError( `Can't have both dotenv files with projectId (env.${projectId}) ` + - `and projectAlias (.env.${projectAlias}) as extensions.` + `and projectAlias (.env.${projectAlias}) as extensions.`, ); } } @@ -276,7 +389,7 @@ export function loadUserEnvs({ try { const data = fs.readFileSync(path.join(functionsSource, f), "utf8"); envs = { ...envs, ...parseStrict(data) }; - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to load environment variables from ${f}.`, { exit: 2, children: err.children?.length > 0 ? err.children : [err], @@ -284,7 +397,7 @@ export function loadUserEnvs({ } } logBullet( - clc.cyan.bold("functions: ") + `Loaded environment variables from ${envFiles.join(", ")}.` + clc.cyan(clc.bold("functions: ")) + `Loaded environment variables from ${envFiles.join(", ")}.`, ); return envs; @@ -297,7 +410,7 @@ export function loadUserEnvs({ */ export function loadFirebaseEnvs( firebaseConfig: Record, - projectId: string + projectId: string, ): Record { return { FIREBASE_CONFIG: JSON.stringify(firebaseConfig), diff --git a/src/functions/events/index.ts b/src/functions/events/index.ts new file mode 100644 index 00000000000..0eb97d89176 --- /dev/null +++ b/src/functions/events/index.ts @@ -0,0 +1,6 @@ +import * as v1 from "./v1"; +import * as v2 from "./v2"; + +export { v1, v2 }; + +export type Event = v1.Event | v2.Event; diff --git a/src/functions/events/v1.ts b/src/functions/events/v1.ts new file mode 100644 index 00000000000..0be24c12487 --- /dev/null +++ b/src/functions/events/v1.ts @@ -0,0 +1,7 @@ +export const BEFORE_CREATE_EVENT = "providers/cloud.auth/eventTypes/user.beforeCreate"; + +export const BEFORE_SIGN_IN_EVENT = "providers/cloud.auth/eventTypes/user.beforeSignIn"; + +export const AUTH_BLOCKING_EVENTS = [BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT] as const; + +export type Event = (typeof AUTH_BLOCKING_EVENTS)[number]; diff --git a/src/functions/events/v2.ts b/src/functions/events/v2.ts new file mode 100644 index 00000000000..84fbd72085f --- /dev/null +++ b/src/functions/events/v2.ts @@ -0,0 +1,44 @@ +export const PUBSUB_PUBLISH_EVENT = "google.cloud.pubsub.topic.v1.messagePublished"; + +export const STORAGE_EVENTS = [ + "google.cloud.storage.object.v1.finalized", + "google.cloud.storage.object.v1.archived", + "google.cloud.storage.object.v1.deleted", + "google.cloud.storage.object.v1.metadataUpdated", +] as const; + +export const FIREBASE_ALERTS_PUBLISH_EVENT = "google.firebase.firebasealerts.alerts.v1.published"; + +export const DATABASE_EVENTS = [ + "google.firebase.database.ref.v1.written", + "google.firebase.database.ref.v1.created", + "google.firebase.database.ref.v1.updated", + "google.firebase.database.ref.v1.deleted", +] as const; + +export const REMOTE_CONFIG_EVENT = "google.firebase.remoteconfig.remoteConfig.v1.updated"; + +export const TEST_LAB_EVENT = "google.firebase.testlab.testMatrix.v1.completed"; + +export const FIRESTORE_EVENTS = [ + "google.cloud.firestore.document.v1.written", + "google.cloud.firestore.document.v1.created", + "google.cloud.firestore.document.v1.updated", + "google.cloud.firestore.document.v1.deleted", + "google.cloud.firestore.document.v1.written.withAuthContext", + "google.cloud.firestore.document.v1.created.withAuthContext", + "google.cloud.firestore.document.v1.updated.withAuthContext", + "google.cloud.firestore.document.v1.deleted.withAuthContext", +] as const; +export const FIRESTORE_EVENT_REGEX = /^google\.cloud\.firestore\.document\.v1\.[^\.]*$/; +export const FIRESTORE_EVENT_WITH_AUTH_CONTEXT_REGEX = + /^google\.cloud\.firestore\.document\.v1\..*\.withAuthContext$/; + +export type Event = + | typeof PUBSUB_PUBLISH_EVENT + | (typeof STORAGE_EVENTS)[number] + | typeof FIREBASE_ALERTS_PUBLISH_EVENT + | (typeof DATABASE_EVENTS)[number] + | typeof REMOTE_CONFIG_EVENT + | typeof TEST_LAB_EVENT + | (typeof FIRESTORE_EVENTS)[number]; diff --git a/src/functions/functionsLog.spec.ts b/src/functions/functionsLog.spec.ts new file mode 100644 index 00000000000..ec9bc4f764e --- /dev/null +++ b/src/functions/functionsLog.spec.ts @@ -0,0 +1,81 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as functionsLog from "./functionslog"; +import { logger } from "../logger"; + +describe("functionsLog", () => { + describe("getApiFilter", () => { + it("should return base api filter for v1&v2 functions", () => { + expect(functionsLog.getApiFilter(undefined)).to.eq( + 'resource.type="cloud_function" OR ' + + '(resource.type="cloud_run_revision" AND ' + + 'labels."goog-managed-by"="cloudfunctions")', + ); + }); + + it("should return list api filter for v1&v2 functions", () => { + expect(functionsLog.getApiFilter("fn1,fn2")).to.eq( + 'resource.type="cloud_function" OR ' + + '(resource.type="cloud_run_revision" AND ' + + 'labels."goog-managed-by"="cloudfunctions")\n' + + '(resource.labels.function_name="fn1" OR ' + + 'resource.labels.service_name="fn1" OR ' + + 'resource.labels.function_name="fn2" OR ' + + 'resource.labels.service_name="fn2")', + ); + }); + }); + + describe("logEntries", () => { + let sandbox: sinon.SinonSandbox; + let loggerStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + loggerStub = sandbox.stub(logger, "info"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should log no entries", () => { + functionsLog.logEntries([]); + + expect(loggerStub).to.have.been.calledOnce; + expect(loggerStub).to.be.calledWith("No log entries found."); + }); + + it("should log entries", () => { + const entries = [ + { + logName: "log1", + resource: { + labels: { + function_name: "fn1", + }, + }, + receiveTimestamp: "0000000", + }, + { + logName: "log2", + resource: { + labels: { + service_name: "fn2", + }, + }, + receiveTimestamp: "0000000", + timestamp: "1111", + severity: "DEBUG", + textPayload: "payload", + }, + ]; + + functionsLog.logEntries(entries); + + expect(loggerStub).to.have.been.calledTwice; + expect(loggerStub.firstCall).to.be.calledWith("1111 D fn2: payload"); + expect(loggerStub.secondCall).to.be.calledWith("--- ? fn1: "); + }); + }); +}); diff --git a/src/functions/functionslog.ts b/src/functions/functionslog.ts index eb7809b60b2..102f76b47d4 100644 --- a/src/functions/functionslog.ts +++ b/src/functions/functionslog.ts @@ -1,25 +1,20 @@ import { logger } from "../logger"; import { LogEntry } from "../gcp/cloudlogging"; -import { previews } from "../previews"; /** * The correct API filter to use when GCFv2 is enabled and/or we want specific function logs - * @param v2Enabled check if the user has the preview v2 enabled * @param functionList list of functions seperated by comma - * @returns the correct filter for use when calling the list api + * @return the correct filter for use when calling the list api */ export function getApiFilter(functionList?: string) { - const baseFilter = previews.functionsv2 - ? 'resource.type="cloud_function" OR ' + - '(resource.type="cloud_run_revision" AND ' + - 'labels."goog-managed-by"="cloudfunctions")' - : 'resource.type="cloud_function"'; + const baseFilter = + 'resource.type="cloud_function" OR ' + + '(resource.type="cloud_run_revision" AND ' + + 'labels."goog-managed-by"="cloudfunctions")'; if (functionList) { const apiFuncFilters = functionList.split(",").map((fn) => { - return previews.functionsv2 - ? `resource.labels.function_name="${fn}" ` + `OR resource.labels.service_name="${fn}"` - : `resource.labels.function_name="${fn}"`; + return `resource.labels.function_name="${fn}" ` + `OR resource.labels.service_name="${fn}"`; }); return baseFilter + `\n(${apiFuncFilters.join(" OR ")})`; } diff --git a/src/functions/projectConfig.spec.ts b/src/functions/projectConfig.spec.ts new file mode 100644 index 00000000000..4c6e568cfb4 --- /dev/null +++ b/src/functions/projectConfig.spec.ts @@ -0,0 +1,123 @@ +import { expect } from "chai"; + +import * as projectConfig from "./projectConfig"; +import { FirebaseError } from "../error"; + +const TEST_CONFIG_0 = { source: "foo" }; + +describe("projectConfig", () => { + describe("normalize", () => { + it("normalizes singleton config", () => { + expect(projectConfig.normalize(TEST_CONFIG_0)).to.deep.equal([TEST_CONFIG_0]); + }); + + it("normalizes array config", () => { + expect(projectConfig.normalize([TEST_CONFIG_0, TEST_CONFIG_0])).to.deep.equal([ + TEST_CONFIG_0, + TEST_CONFIG_0, + ]); + }); + + it("throws error if given empty config", () => { + expect(() => projectConfig.normalize([])).to.throw(FirebaseError); + }); + }); + + describe("validate", () => { + it("passes validation for simple config", () => { + expect(projectConfig.validate([TEST_CONFIG_0])).to.deep.equal([TEST_CONFIG_0]); + }); + + it("fails validation given config w/o source", () => { + expect(() => projectConfig.validate([{ runtime: "nodejs10" }])).to.throw( + FirebaseError, + /codebase source must be specified/, + ); + }); + + it("fails validation given config w/ empty source", () => { + expect(() => projectConfig.validate([{ source: "" }])).to.throw( + FirebaseError, + /codebase source must be specified/, + ); + }); + + it("fails validation given config w/ duplicate source", () => { + expect(() => + projectConfig.validate([TEST_CONFIG_0, { ...TEST_CONFIG_0, codebase: "unique-codebase" }]), + ).to.throw(FirebaseError, /source must be unique/); + }); + + it("fails validation given codebase name with capital letters", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, codebase: "ABCDE" }])).to.throw( + FirebaseError, + /Invalid codebase name/, + ); + }); + + it("fails validation given codebase name with invalid characters", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, codebase: "abc.efg" }])).to.throw( + FirebaseError, + /Invalid codebase name/, + ); + }); + + it("fails validation given long codebase name", () => { + expect(() => + projectConfig.validate([ + { + ...TEST_CONFIG_0, + codebase: "thisismorethan63characterslongxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + }, + ]), + ).to.throw(FirebaseError, /Invalid codebase name/); + }); + }); + + describe("normalizeAndValidate", () => { + it("returns normalized config for singleton config", () => { + expect(projectConfig.normalizeAndValidate(TEST_CONFIG_0)).to.deep.equal([TEST_CONFIG_0]); + }); + + it("returns normalized config for multi-resource config", () => { + expect(projectConfig.normalizeAndValidate([TEST_CONFIG_0])).to.deep.equal([TEST_CONFIG_0]); + }); + + it("fails validation given singleton config w/o source", () => { + expect(() => projectConfig.normalizeAndValidate({ runtime: "nodejs10" })).to.throw( + FirebaseError, + /codebase source must be specified/, + ); + }); + + it("fails validation given singleton config w empty source", () => { + expect(() => projectConfig.normalizeAndValidate({ source: "" })).to.throw( + FirebaseError, + /codebase source must be specified/, + ); + }); + + it("fails validation given multi-resource config w/o source", () => { + expect(() => projectConfig.normalizeAndValidate([{ runtime: "nodejs10" }])).to.throw( + FirebaseError, + /codebase source must be specified/, + ); + }); + + it("fails validation given config w/ duplicate source", () => { + expect(() => projectConfig.normalizeAndValidate([TEST_CONFIG_0, TEST_CONFIG_0])).to.throw( + FirebaseError, + /functions.source must be unique/, + ); + }); + + it("fails validation given config w/ duplicate codebase", () => { + expect(() => + projectConfig.normalizeAndValidate([ + { ...TEST_CONFIG_0, codebase: "foo" }, + { ...TEST_CONFIG_0, codebase: "foo", source: "bar" }, + ]), + ).to.throw(FirebaseError, /functions.codebase must be unique/); + }); + }); +}); diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts new file mode 100644 index 00000000000..313db95d48a --- /dev/null +++ b/src/functions/projectConfig.ts @@ -0,0 +1,103 @@ +import { FunctionsConfig, FunctionConfig } from "../firebaseConfig"; +import { FirebaseError } from "../error"; + +export type NormalizedConfig = [FunctionConfig, ...FunctionConfig[]]; +export type ValidatedSingle = FunctionConfig & { source: string; codebase: string }; +export type ValidatedConfig = [ValidatedSingle, ...ValidatedSingle[]]; + +export const DEFAULT_CODEBASE = "default"; + +/** + * Normalize functions config to return functions config in an array form. + */ +export function normalize(config?: FunctionsConfig): NormalizedConfig { + if (!config) { + throw new FirebaseError("No valid functions configuration detected in firebase.json"); + } + + if (Array.isArray(config)) { + if (config.length < 1) { + throw new FirebaseError("Requires at least one functions.source in firebase.json."); + } + // Unfortunately, Typescript can't figure out that config has at least one element. We assert the type manually. + return config as NormalizedConfig; + } + return [config]; +} + +/** + * Check that the codebase name is less than 64 characters and only contains allowed characters. + */ +export function validateCodebase(codebase: string): void { + if (codebase.length === 0 || codebase.length > 63 || !/^[a-z0-9_-]+$/.test(codebase)) { + throw new FirebaseError( + "Invalid codebase name. Codebase must be less than 64 characters and " + + "can contain only lowercase letters, numeric characters, underscores, and dashes.", + ); + } +} + +function validateSingle(config: FunctionConfig): ValidatedSingle { + if (!config.source) { + throw new FirebaseError("codebase source must be specified"); + } + if (!config.codebase) { + config.codebase = DEFAULT_CODEBASE; + } + validateCodebase(config.codebase); + + return { ...config, source: config.source, codebase: config.codebase }; +} + +/** + * Check that the property is unique in the given config. + */ +export function assertUnique( + config: ValidatedConfig, + property: keyof ValidatedSingle, + propval?: string, +): void { + const values = new Set(); + if (propval) { + values.add(propval); + } + for (const single of config) { + const value = single[property]; + if (values.has(value)) { + throw new FirebaseError( + `functions.${property} must be unique but '${value}' was used more than once.`, + ); + } + values.add(value); + } +} + +/** + * Validate functions config. + */ +export function validate(config: NormalizedConfig): ValidatedConfig { + const validated = config.map((cfg) => validateSingle(cfg)) as ValidatedConfig; + assertUnique(validated, "source"); + assertUnique(validated, "codebase"); + return validated; +} + +/** + * Normalize and validate functions config. + * + * Valid functions config has exactly one config and has all required fields set. + */ +export function normalizeAndValidate(config?: FunctionsConfig): ValidatedConfig { + return validate(normalize(config)); +} + +/** + * Return functions config for given codebase. + */ +export function configForCodebase(config: ValidatedConfig, codebase: string): ValidatedSingle { + const codebaseCfg = config.find((c) => c.codebase === codebase); + if (!codebaseCfg) { + throw new FirebaseError(`No functions config found for codebase ${codebase}`); + } + return codebaseCfg; +} diff --git a/src/functions/python.ts b/src/functions/python.ts new file mode 100644 index 00000000000..6baa355b0a0 --- /dev/null +++ b/src/functions/python.ts @@ -0,0 +1,47 @@ +import * as path from "path"; +import * as spawn from "cross-spawn"; +import * as cp from "child_process"; +import { logger } from "../logger"; +import { IS_WINDOWS } from "../utils"; + +/** + * Default directory for python virtual environment. + */ +export const DEFAULT_VENV_DIR = "venv"; + +/** + * Get command for running Python virtual environment for given platform. + */ +export function virtualEnvCmd(cwd: string, venvDir: string): { command: string; args: string[] } { + const activateScriptPath = IS_WINDOWS ? ["Scripts", "activate.bat"] : ["bin", "activate"]; + const venvActivate = `"${path.join(cwd, venvDir, ...activateScriptPath)}"`; + return { + command: IS_WINDOWS ? venvActivate : ".", + args: [IS_WINDOWS ? "" : venvActivate], + }; +} + +/** + * Spawn a process inside the Python virtual environment if found. + */ +export function runWithVirtualEnv( + commandAndArgs: string[], + cwd: string, + envs: Record, + spawnOpts: cp.SpawnOptions = {}, + venvDir = DEFAULT_VENV_DIR, +): cp.ChildProcess { + const { command, args } = virtualEnvCmd(cwd, venvDir); + args.push("&&", ...commandAndArgs); + logger.debug(`Running command with virtualenv: command=${command}, args=${JSON.stringify(args)}`); + + return spawn(command, args, { + shell: true, + cwd, + stdio: [/* stdin= */ "pipe", /* stdout= */ "pipe", /* stderr= */ "pipe", "pipe"], + ...spawnOpts, + // Linting disabled since internal types expect NODE_ENV which does not apply to Python runtimes. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + env: envs as any, + }); +} diff --git a/src/test/functions/runtimeConfigExport.spec.ts b/src/functions/runtimeConfigExport.spec.ts similarity index 94% rename from src/test/functions/runtimeConfigExport.spec.ts rename to src/functions/runtimeConfigExport.spec.ts index 34e42d5146a..73ce06c1ae1 100644 --- a/src/test/functions/runtimeConfigExport.spec.ts +++ b/src/functions/runtimeConfigExport.spec.ts @@ -1,9 +1,9 @@ import { expect } from "chai"; -import * as configExport from "../../../src/functions/runtimeConfigExport"; -import * as env from "../../../src/functions/env"; +import * as configExport from "./runtimeConfigExport"; +import * as env from "./env"; import * as sinon from "sinon"; -import * as rc from "../../rc"; +import * as rc from "../rc"; describe("functions-config-export", () => { describe("getAllProjects", () => { @@ -81,7 +81,7 @@ describe("functions-config-export", () => { it("should convert valid functions config ", () => { const { success, errors } = configExport.configToEnv( { foo: { bar: "foobar" }, service: { api: { url: "foobar", name: "a service" } } }, - "" + "", ); expect(success).to.have.deep.members([ { origKey: "service.api.url", newKey: "SERVICE_API_URL", value: "foobar" }, @@ -94,7 +94,7 @@ describe("functions-config-export", () => { it("should collect errors for invalid conversions", () => { const { success, errors } = configExport.configToEnv( { firebase: { name: "foobar" }, service: { api: { url: "foobar", name: "a service" } } }, - "" + "", ); expect(success).to.have.deep.members([ { origKey: "service.api.url", newKey: "SERVICE_API_URL", value: "foobar" }, @@ -106,7 +106,7 @@ describe("functions-config-export", () => { it("should use prefix to fix invalid keys", () => { const { success, errors } = configExport.configToEnv( { firebase: { name: "foobar" }, service: { api: { url: "foobar", name: "a service" } } }, - "CONFIG_" + "CONFIG_", ); expect(success).to.have.deep.members([ { origKey: "service.api.url", newKey: "SERVICE_API_URL", value: "foobar" }, @@ -133,11 +133,11 @@ describe("functions-config-export", () => { it("should preserve newline characters", () => { const dotenv = configExport.toDotenvFormat([ - { origKey: "service.api.url", newKey: "SERVICE_API_URL", value: "hello\nworld" }, + { origKey: "service.api.url", newKey: "SERVICE_API_URL", value: "hello\nthere\nworld" }, ]); const { envs, errors } = env.parse(dotenv); expect(envs).to.be.deep.equal({ - SERVICE_API_URL: "hello\nworld", + SERVICE_API_URL: "hello\nthere\nworld", }); expect(errors).to.be.empty; }); @@ -146,13 +146,13 @@ describe("functions-config-export", () => { describe("generateDotenvFilename", () => { it("should generate dotenv filename using project alias", () => { expect( - configExport.generateDotenvFilename({ projectId: "my-project", alias: "prod" }) + configExport.generateDotenvFilename({ projectId: "my-project", alias: "prod" }), ).to.equal(".env.prod"); }); it("should generate dotenv filename using project id if alias doesn't exist", () => { expect(configExport.generateDotenvFilename({ projectId: "my-project" })).to.equal( - ".env.my-project" + ".env.my-project", ); }); }); diff --git a/src/functions/runtimeConfigExport.ts b/src/functions/runtimeConfigExport.ts index 935acd594b4..cc4b4be47ac 100644 --- a/src/functions/runtimeConfigExport.ts +++ b/src/functions/runtimeConfigExport.ts @@ -1,7 +1,4 @@ -import * as fs from "fs"; -import * as path from "path"; - -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as env from "./env"; import * as functionsConfig from "../functionsConfig"; @@ -47,7 +44,7 @@ export function getProjectInfos(options: { if (Object.keys(result).includes(projectId)) { logWarning( `Multiple aliases found for ${clc.bold(projectId)}. ` + - `Preferring alias (${clc.bold(result[projectId])}) over (${clc.bold(alias)}).` + `Preferring alias (${clc.bold(result[projectId])}) over (${clc.bold(alias)}).`, ); continue; } @@ -84,7 +81,7 @@ export async function hydrateConfigs(pInfos: ProjectConfigInfo[]): Promise }) .catch((err) => { logger.debug( - `Failed to fetch runtime config for project ${info.projectId}: ${err.message}` + `Failed to fetch runtime config for project ${info.projectId}: ${err.message}`, ); }); }); @@ -107,7 +104,7 @@ export function convertKey(configKey: string, prefix: string): string { let envKey = baseKey; try { env.validateKey(envKey); - } catch (err) { + } catch (err: any) { if (err instanceof env.KeyValidationError) { envKey = prefix + envKey; env.validateKey(envKey); @@ -127,7 +124,7 @@ export function configToEnv(configs: Record, prefix: string): C try { const envKey = convertKey(configKey, prefix); success.push({ origKey: configKey, newKey: envKey, value: value as string }); - } catch (err) { + } catch (err: any) { if (err instanceof env.KeyValidationError) { errors.push({ origKey: configKey, @@ -169,14 +166,19 @@ export function hydrateEnvs(pInfos: ProjectConfigInfo[], prefix: string): string return errMsg; } +const CHARACTERS_TO_ESCAPE_SEQUENCES: Record = { + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\v": "\\v", + "\\": "\\\\", + '"': '\\"', + "'": "\\'", +}; + function escape(s: string): string { - // Escape newlines and tabs - const result = s - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t") - .replace("\v", "\\v"); - return result.replace(/(['"])/g, "\\$1"); + // Escape newlines, tabs, backslashes and quotes + return s.replace(/[\n\r\t\v\\"']/g, (ch) => CHARACTERS_TO_ESCAPE_SEQUENCES[ch]); } /** diff --git a/src/functions/secrets.spec.ts b/src/functions/secrets.spec.ts new file mode 100644 index 00000000000..31e7efe17f4 --- /dev/null +++ b/src/functions/secrets.spec.ts @@ -0,0 +1,569 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as secretManager from "../gcp/secretManager"; +import * as gcf from "../gcp/cloudfunctions"; +import * as secrets from "./secrets"; +import * as utils from "../utils"; +import * as prompt from "../prompt"; +import * as backend from "../deploy/functions/backend"; +import * as poller from "../operation-poller"; +import { Options } from "../options"; +import { FirebaseError } from "../error"; +import { updateEndpointSecret } from "./secrets"; + +const ENDPOINT = { + id: "id", + region: "region", + project: "project", + entryPoint: "id", + runtime: "nodejs16" as const, + platform: "gcfv1" as const, + httpsTrigger: {}, +}; + +describe("functions/secret", () => { + const options = { force: false } as Options; + + describe("ensureValidKey", () => { + let warnStub: sinon.SinonStub; + let promptStub: sinon.SinonStub; + + beforeEach(() => { + warnStub = sinon.stub(utils, "logWarning").resolves(undefined); + promptStub = sinon.stub(prompt, "promptOnce").resolves(true); + }); + + afterEach(() => { + warnStub.restore(); + promptStub.restore(); + }); + + it("returns the original key if it follows convention", async () => { + expect(await secrets.ensureValidKey("MY_SECRET_KEY", options)).to.equal("MY_SECRET_KEY"); + expect(warnStub).to.not.have.been.called; + }); + + it("returns the transformed key (with warning) if with dashes", async () => { + expect(await secrets.ensureValidKey("MY-SECRET-KEY", options)).to.equal("MY_SECRET_KEY"); + expect(warnStub).to.have.been.calledOnce; + }); + + it("returns the transformed key (with warning) if with periods", async () => { + expect(await secrets.ensureValidKey("MY.SECRET.KEY", options)).to.equal("MY_SECRET_KEY"); + expect(warnStub).to.have.been.calledOnce; + }); + + it("returns the transformed key (with warning) if with lower cases", async () => { + expect(await secrets.ensureValidKey("my_secret_key", options)).to.equal("MY_SECRET_KEY"); + expect(warnStub).to.have.been.calledOnce; + }); + + it("returns the transformed key (with warning) if camelCased", async () => { + expect(await secrets.ensureValidKey("mySecretKey", options)).to.equal("MY_SECRET_KEY"); + expect(warnStub).to.have.been.calledOnce; + }); + + it("throws error if given non-conventional key w/ forced option", async () => { + await expect( + secrets.ensureValidKey("throwError", { ...options, force: true }), + ).to.be.rejectedWith(FirebaseError); + }); + + it("throws error if given reserved key", async () => { + await expect(secrets.ensureValidKey("FIREBASE_CONFIG", options)).to.be.rejectedWith( + FirebaseError, + ); + }); + }); + + describe("ensureSecret", () => { + const secret: secretManager.Secret = { + projectId: "project-id", + name: "MY_SECRET", + labels: secretManager.labels("functions"), + replication: {}, + }; + + let sandbox: sinon.SinonSandbox; + let getStub: sinon.SinonStub; + let createStub: sinon.SinonStub; + let patchStub: sinon.SinonStub; + let promptStub: sinon.SinonStub; + let warnStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + getStub = sandbox.stub(secretManager, "getSecret").rejects("Unexpected call"); + createStub = sandbox.stub(secretManager, "createSecret").rejects("Unexpected call"); + patchStub = sandbox.stub(secretManager, "patchSecret").rejects("Unexpected call"); + + promptStub = sandbox.stub(prompt, "promptOnce").resolves(true); + warnStub = sandbox.stub(utils, "logWarning").resolves(undefined); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("returns existing secret if we have one", async () => { + getStub.resolves(secret); + + await expect( + secrets.ensureSecret("project-id", "MY_SECRET", options), + ).to.eventually.deep.equal(secret); + expect(getStub).to.have.been.calledOnce; + }); + + it("prompt user to have Firebase manage the secret if not managed by Firebase", async () => { + getStub.resolves({ ...secret, labels: [] }); + patchStub.resolves(secret); + + await expect( + secrets.ensureSecret("project-id", "MY_SECRET", options), + ).to.eventually.deep.equal(secret); + expect(warnStub).to.have.been.calledOnce; + expect(promptStub).to.have.been.calledOnce; + }); + + it("does not prompt user to have Firebase manage the secret if already managed by Firebase", async () => { + getStub.resolves({ ...secret, labels: secretManager.labels() }); + patchStub.resolves(secret); + + await expect( + secrets.ensureSecret("project-id", "MY_SECRET", options), + ).to.eventually.deep.equal(secret); + expect(warnStub).not.to.have.been.calledOnce; + expect(promptStub).not.to.have.been.calledOnce; + }); + + it("creates a new secret if it doesn't exists", async () => { + getStub.rejects({ status: 404 }); + createStub.resolves(secret); + + await expect( + secrets.ensureSecret("project-id", "MY_SECRET", options), + ).to.eventually.deep.equal(secret); + }); + + it("throws if it cannot reach Secret Manager", async () => { + getStub.rejects({ status: 500 }); + + await expect(secrets.ensureSecret("project-id", "MY_SECRET", options)).to.eventually.be + .rejected; + }); + }); + + describe("of", () => { + function makeSecret(name: string, version?: string): backend.SecretEnvVar { + return { + projectId: "project", + key: name, + secret: name, + version: version ?? "1", + }; + } + + it("returns empty list given empty list", () => { + expect(secrets.of([])).to.be.empty; + }); + + it("collects all secret environment variables", () => { + const secret1 = makeSecret("SECRET1"); + const secret2 = makeSecret("SECRET2"); + const secret3 = makeSecret("SECRET3"); + + const endpoints: backend.Endpoint[] = [ + { + ...ENDPOINT, + secretEnvironmentVariables: [secret1], + }, + ENDPOINT, + { + ...ENDPOINT, + secretEnvironmentVariables: [secret2, secret3], + }, + ]; + expect(secrets.of(endpoints)).to.have.members([secret1, secret2, secret3]); + expect(secrets.of(endpoints)).to.have.length(3); + }); + }); + + describe("getSecretVersions", () => { + function makeSecret(name: string, version?: string): backend.SecretEnvVar { + const secret: backend.SecretEnvVar = { + projectId: "project", + key: name, + secret: name, + }; + if (version) { + secret.version = version; + } + return secret; + } + + it("returns object mapping secrets and their versions", () => { + const secret1 = makeSecret("SECRET1", "1"); + const secret2 = makeSecret("SECRET2", "100"); + const secret3 = makeSecret("SECRET3", "2"); + + const endpoint = { + ...ENDPOINT, + secretEnvironmentVariables: [secret1, secret2, secret3], + }; + + expect(secrets.getSecretVersions(endpoint)).to.deep.eq({ + [secret1.secret]: secret1.version, + [secret2.secret]: secret2.version, + [secret3.secret]: secret3.version, + }); + }); + }); + + describe("pruneSecrets", () => { + let listSecretsStub: sinon.SinonStub; + let listSecretVersionsStub: sinon.SinonStub; + let getSecretVersionStub: sinon.SinonStub; + + const secret1: secretManager.Secret = { + projectId: "project", + name: "MY_SECRET1", + labels: {}, + replication: {}, + }; + const secretVersion11: secretManager.SecretVersion = { + secret: secret1, + versionId: "1", + createTime: "2024-03-28T19:43:26", + }; + const secretVersion12: secretManager.SecretVersion = { + secret: secret1, + versionId: "2", + createTime: "2024-03-28T19:43:26", + }; + + const secret2: secretManager.Secret = { + projectId: "project", + name: "MY_SECRET2", + labels: {}, + replication: {}, + }; + const secretVersion21: secretManager.SecretVersion = { + secret: secret2, + versionId: "1", + createTime: "2024-03-28T19:43:26", + }; + + function toSecretEnvVar(sv: secretManager.SecretVersion): backend.SecretEnvVar { + return { + projectId: "project", + version: sv.versionId, + secret: sv.secret.name, + key: sv.secret.name, + }; + } + + beforeEach(() => { + listSecretsStub = sinon.stub(secretManager, "listSecrets").rejects("Unexpected call"); + listSecretVersionsStub = sinon + .stub(secretManager, "listSecretVersions") + .rejects("Unexpected call"); + getSecretVersionStub = sinon + .stub(secretManager, "getSecretVersion") + .rejects("Unexpected call"); + }); + + afterEach(() => { + listSecretsStub.restore(); + listSecretVersionsStub.restore(); + getSecretVersionStub.restore(); + }); + + it("returns nothing if unused", async () => { + listSecretsStub.resolves([]); + + await expect( + secrets.pruneSecrets({ projectId: "project", projectNumber: "12345" }, []), + ).to.eventually.deep.equal([]); + }); + + it("returns all secrets given no endpoints", async () => { + listSecretsStub.resolves([secret1, secret2]); + listSecretVersionsStub.onFirstCall().resolves([secretVersion11, secretVersion12]); + listSecretVersionsStub.onSecondCall().resolves([secretVersion21]); + + const pruned = await secrets.pruneSecrets( + { projectId: "project", projectNumber: "12345" }, + [], + ); + + expect(pruned).to.have.deep.members( + [secretVersion11, secretVersion12, secretVersion21].map(toSecretEnvVar), + ); + expect(pruned).to.have.length(3); + }); + + it("does not include secret version in use", async () => { + listSecretsStub.resolves([secret1, secret2]); + listSecretVersionsStub.onFirstCall().resolves([secretVersion11, secretVersion12]); + listSecretVersionsStub.onSecondCall().resolves([secretVersion21]); + + const pruned = await secrets.pruneSecrets({ projectId: "project", projectNumber: "12345" }, [ + { ...ENDPOINT, secretEnvironmentVariables: [toSecretEnvVar(secretVersion12)] }, + ]); + + expect(pruned).to.have.deep.members([secretVersion11, secretVersion21].map(toSecretEnvVar)); + expect(pruned).to.have.length(2); + }); + + it("resolves 'latest' secrets and properly prunes it", async () => { + listSecretsStub.resolves([secret1, secret2]); + listSecretVersionsStub.onFirstCall().resolves([secretVersion11, secretVersion12]); + listSecretVersionsStub.onSecondCall().resolves([secretVersion21]); + getSecretVersionStub.resolves(secretVersion12); + + const pruned = await secrets.pruneSecrets({ projectId: "project", projectNumber: "12345" }, [ + { + ...ENDPOINT, + secretEnvironmentVariables: [{ ...toSecretEnvVar(secretVersion12), version: "latest" }], + }, + ]); + + expect(pruned).to.have.deep.members([secretVersion11, secretVersion21].map(toSecretEnvVar)); + expect(pruned).to.have.length(2); + }); + }); + + describe("inUse", () => { + const projectId = "project"; + const projectNumber = "12345"; + const secret: secretManager.Secret = { + projectId, + name: "MY_SECRET", + labels: {}, + replication: {}, + }; + + it("returns true if secret is in use", () => { + expect( + secrets.inUse({ projectId, projectNumber }, secret, { + ...ENDPOINT, + secretEnvironmentVariables: [ + { projectId, key: secret.name, secret: secret.name, version: "1" }, + ], + }), + ).to.be.true; + }); + + it("returns true if secret is in use by project number", () => { + expect( + secrets.inUse({ projectId, projectNumber }, secret, { + ...ENDPOINT, + secretEnvironmentVariables: [ + { projectId: projectNumber, key: secret.name, secret: secret.name, version: "1" }, + ], + }), + ).to.be.true; + }); + + it("returns false if secret is not in use", () => { + expect(secrets.inUse({ projectId, projectNumber }, secret, ENDPOINT)).to.be.false; + }); + + it("returns false if secret of same name from another project is in use", () => { + expect( + secrets.inUse({ projectId, projectNumber }, secret, { + ...ENDPOINT, + secretEnvironmentVariables: [ + { projectId: "another-project", key: secret.name, secret: secret.name, version: "1" }, + ], + }), + ).to.be.false; + }); + }); + + describe("versionInUse", () => { + const projectId = "project"; + const projectNumber = "12345"; + const sv: secretManager.SecretVersion = { + versionId: "5", + secret: { + projectId, + name: "MY_SECRET", + labels: {}, + replication: {}, + }, + createTime: "2024-03-28T19:43:26", + }; + + it("returns true if secret version is in use", () => { + expect( + secrets.versionInUse({ projectId, projectNumber }, sv, { + ...ENDPOINT, + secretEnvironmentVariables: [ + { projectId, key: sv.secret.name, secret: sv.secret.name, version: "5" }, + ], + }), + ).to.be.true; + }); + + it("returns true if secret version is in use by project number", () => { + expect( + secrets.versionInUse({ projectId, projectNumber }, sv, { + ...ENDPOINT, + secretEnvironmentVariables: [ + { projectId: projectNumber, key: sv.secret.name, secret: sv.secret.name, version: "5" }, + ], + }), + ).to.be.true; + }); + + it("returns false if secret version is not in use", () => { + expect(secrets.versionInUse({ projectId, projectNumber }, sv, ENDPOINT)).to.be.false; + }); + + it("returns false if a different version of the secret is in use", () => { + expect( + secrets.versionInUse({ projectId, projectNumber }, sv, { + ...ENDPOINT, + secretEnvironmentVariables: [ + { projectId, key: sv.secret.name, secret: sv.secret.name, version: "1" }, + ], + }), + ).to.be.false; + }); + }); + + describe("pruneAndDestroySecrets", () => { + let pruneSecretsStub: sinon.SinonStub; + let destroySecretVersionStub: sinon.SinonStub; + + const projectId = "projectId"; + const projectNumber = "12345"; + const secret0: backend.SecretEnvVar = { + projectId, + key: "MY_SECRET", + secret: "MY_SECRET", + version: "1", + }; + const secret1: backend.SecretEnvVar = { + projectId, + key: "MY_SECRET", + secret: "MY_SECRET", + version: "1", + }; + + beforeEach(() => { + pruneSecretsStub = sinon.stub(secrets, "pruneSecrets").rejects("Unexpected call"); + destroySecretVersionStub = sinon + .stub(secretManager, "destroySecretVersion") + .rejects("Unexpected call"); + }); + + afterEach(() => { + pruneSecretsStub.restore(); + destroySecretVersionStub.restore(); + }); + + it("destroys pruned secrets", async () => { + pruneSecretsStub.resolves([secret1]); + destroySecretVersionStub.resolves(); + + await expect( + secrets.pruneAndDestroySecrets({ projectId, projectNumber }, [ + { + ...ENDPOINT, + secretEnvironmentVariables: [secret0], + }, + { + ...ENDPOINT, + secretEnvironmentVariables: [secret1], + }, + ]), + ).to.eventually.deep.equal({ erred: [], destroyed: [secret1] }); + }); + + it("collects errors", async () => { + pruneSecretsStub.resolves([secret0, secret1]); + destroySecretVersionStub.onFirstCall().resolves(); + destroySecretVersionStub.onSecondCall().rejects({ message: "an error" }); + + await expect( + secrets.pruneAndDestroySecrets({ projectId, projectNumber }, [ + { + ...ENDPOINT, + secretEnvironmentVariables: [secret0], + }, + { + ...ENDPOINT, + secretEnvironmentVariables: [secret1], + }, + ]), + ).to.eventually.deep.equal({ erred: [{ message: "an error" }], destroyed: [secret0] }); + }); + }); + + describe("updateEndpointsSecret", () => { + const projectId = "project"; + const projectNumber = "12345"; + const secretVersion: secretManager.SecretVersion = { + secret: { + projectId, + name: "MY_SECRET", + labels: {}, + replication: {}, + }, + versionId: "2", + createTime: "2024-03-28T19:43:26", + }; + + let gcfMock: sinon.SinonMock; + let pollerStub: sinon.SinonStub; + + beforeEach(() => { + gcfMock = sinon.mock(gcf); + pollerStub = sinon.stub(poller, "pollOperation").rejects("Unexpected call"); + }); + + afterEach(() => { + gcfMock.verify(); + gcfMock.restore(); + pollerStub.restore(); + }); + + it("returns early if secret is not in use", async () => { + const endpoint: backend.Endpoint = { + ...ENDPOINT, + secretEnvironmentVariables: [], + }; + + gcfMock.expects("updateFunction").never(); + await updateEndpointSecret({ projectId, projectNumber }, secretVersion, endpoint); + }); + + it("updates function with the version of the given secret", async () => { + const sev: backend.SecretEnvVar = { + projectId: projectNumber, + secret: secretVersion.secret.name, + key: secretVersion.secret.name, + version: "1", + }; + const endpoint: backend.Endpoint = { + ...ENDPOINT, + secretEnvironmentVariables: [sev], + }; + const fn: Omit = { + name: `projects/${endpoint.project}/locations/${endpoint.region}/functions/${endpoint.id}`, + runtime: endpoint.runtime, + entryPoint: endpoint.entryPoint, + secretEnvironmentVariables: [{ ...sev, version: "2" }], + }; + + pollerStub.resolves({ ...fn, httpsTrigger: {} }); + gcfMock.expects("updateFunction").once().withArgs(fn).resolves({}); + + await updateEndpointSecret({ projectId, projectNumber }, secretVersion, endpoint); + }); + }); +}); diff --git a/src/functions/secrets.ts b/src/functions/secrets.ts new file mode 100644 index 00000000000..5e8cf85145e --- /dev/null +++ b/src/functions/secrets.ts @@ -0,0 +1,394 @@ +import * as utils from "../utils"; +import * as poller from "../operation-poller"; +import * as gcfV1 from "../gcp/cloudfunctions"; +import * as gcfV2 from "../gcp/cloudfunctionsv2"; +import * as backend from "../deploy/functions/backend"; +import { functionsOrigin, functionsV2Origin } from "../api"; +import { + createSecret, + destroySecretVersion, + getSecret, + getSecretVersion, + isAppHostingManaged, + listSecrets, + listSecretVersions, + parseSecretResourceName, + patchSecret, + Secret, + SecretVersion, +} from "../gcp/secretManager"; +import { Options } from "../options"; +import { FirebaseError } from "../error"; +import { logWarning } from "../utils"; +import { promptOnce } from "../prompt"; +import { validateKey } from "./env"; +import { logger } from "../logger"; +import { assertExhaustive } from "../functional"; +import { isFunctionsManaged, FIREBASE_MANAGED } from "../gcp/secretManager"; +import { labels } from "../gcp/secretManager"; +import { needProjectId } from "../projectUtils"; + +const Table = require("cli-table"); + +// For mysterious reasons, importing the poller option in fabricator.ts leads to some +// value of the poller option to be undefined at runtime. I can't figure out what's going on, +// but don't have time to find out. Taking a shortcut and copying the values directly in +// violation of DRY. Sorry! +const gcfV1PollerOptions: Omit = { + apiOrigin: functionsOrigin(), + apiVersion: "v1", + masterTimeout: 25 * 60 * 1_000, // 25 minutes is the maximum build time for a function + maxBackoff: 10_000, +}; + +const gcfV2PollerOptions: Omit = { + apiOrigin: functionsV2Origin(), + apiVersion: "v2", + masterTimeout: 25 * 60 * 1_000, // 25 minutes is the maximum build time for a function + maxBackoff: 10_000, +}; + +type ProjectInfo = { + projectId: string; + projectNumber: string; +}; + +function toUpperSnakeCase(key: string): string { + return key + .replace(/[.-]/g, "_") + .replace(/([a-z])([A-Z])/g, "$1_$2") + .toUpperCase(); +} + +/** + * Validate and transform keys to match the convention recommended by Firebase. + */ +export async function ensureValidKey(key: string, options: Options): Promise { + const transformedKey = toUpperSnakeCase(key); + if (transformedKey !== key) { + if (options.force) { + throw new FirebaseError("Secret key must be in UPPER_SNAKE_CASE."); + } + logWarning(`By convention, secret key must be in UPPER_SNAKE_CASE.`); + const confirm = await promptOnce( + { + name: "updateKey", + type: "confirm", + default: true, + message: `Would you like to use ${transformedKey} as key instead?`, + }, + options, + ); + if (!confirm) { + throw new FirebaseError("Secret key must be in UPPER_SNAKE_CASE."); + } + } + try { + validateKey(transformedKey); + } catch (err: any) { + throw new FirebaseError(`Invalid secret key ${transformedKey}`, { children: [err] }); + } + return transformedKey; +} + +/** + * Ensure secret exists. Optionally prompt user to have non-Firebase managed keys be managed by Firebase. + */ +export async function ensureSecret( + projectId: string, + name: string, + options: Options, +): Promise { + try { + const secret = await getSecret(projectId, name); + if (isAppHostingManaged(secret)) { + logWarning( + "Your secret is managed by Firebase App Hosting. Continuing will disable automatic deletion of old versions.", + ); + const stopTracking = await promptOnce( + { + name: "doNotTrack", + type: "confirm", + default: false, + message: "Do you wish to continue?", + }, + options, + ); + if (stopTracking) { + delete secret.labels[FIREBASE_MANAGED]; + await patchSecret(secret.projectId, secret.name, secret.labels); + } else { + throw new Error( + "A secret cannot be managed by both Firebase App Hosting and Cloud Functions for Firebase", + ); + } + } else if (!isFunctionsManaged(secret)) { + if (!options.force) { + logWarning( + "Your secret is not managed by Cloud Functions for Firebase. " + + "Firebase managed secrets are automatically pruned to reduce your monthly cost for using Secret Manager. ", + ); + const confirm = await promptOnce( + { + name: "updateLabels", + type: "confirm", + default: true, + message: `Would you like to have your secret ${secret.name} managed by Cloud Functions for Firebase?`, + }, + options, + ); + if (confirm) { + return patchSecret(projectId, secret.name, { + ...secret.labels, + ...labels(), + }); + } + } + } + return secret; + } catch (err: any) { + if (err.status !== 404) { + throw err; + } + } + return await createSecret(projectId, name, labels()); +} + +/** + * Collects all secret environment variables of endpoints. + */ +export function of(endpoints: backend.Endpoint[]): backend.SecretEnvVar[] { + return endpoints.reduce( + (envs, endpoint) => [...envs, ...(endpoint.secretEnvironmentVariables || [])], + [] as backend.SecretEnvVar[], + ); +} + +/** + * Generates an object mapping secret's with their versions. + */ +export function getSecretVersions(endpoint: backend.Endpoint): Record { + return (endpoint.secretEnvironmentVariables || []).reduce( + (memo, { secret, version }) => { + memo[secret] = version || ""; + return memo; + }, + {} as Record, + ); +} + +/** + * Checks whether a secret is in use by the given endpoint. + */ +export function inUse(projectInfo: ProjectInfo, secret: Secret, endpoint: backend.Endpoint) { + const { projectId, projectNumber } = projectInfo; + for (const sev of of([endpoint])) { + if ( + (sev.projectId === projectId || sev.projectId === projectNumber) && + sev.secret === secret.name + ) { + return true; + } + } + return false; +} + +/** + * Checks whether a secret version in use by the given endpoint. + */ +export function versionInUse( + projectInfo: ProjectInfo, + sv: SecretVersion, + endpoint: backend.Endpoint, +): boolean { + const { projectId, projectNumber } = projectInfo; + for (const sev of of([endpoint])) { + if ( + (sev.projectId === projectId || sev.projectId === projectNumber) && + sev.secret === sv.secret.name && + sev.version === sv.versionId + ) { + return true; + } + } + return false; +} + +/** + * Returns all secret versions from Firebase managed secrets unused in the given list of endpoints. + */ +export async function pruneSecrets( + projectInfo: ProjectInfo, + endpoints: backend.Endpoint[], +): Promise[]> { + const { projectId, projectNumber } = projectInfo; + const pruneKey = (name: string, version: string) => `${name}@${version}`; + const prunedSecrets: Set = new Set(); + + // Collect all Firebase managed secret versions + const haveSecrets = await listSecrets(projectId, `labels.${FIREBASE_MANAGED}=true`); + for (const secret of haveSecrets) { + const versions = await listSecretVersions(projectId, secret.name, `NOT state: DESTROYED`); + for (const version of versions) { + prunedSecrets.add(pruneKey(secret.name, version.versionId)); + } + } + + // Prune all project-scoped secrets in use. + const secrets: Required[] = []; + for (const secret of of(endpoints)) { + if (!secret.version) { + // All bets are off if secret version isn't available in the endpoint definition. + // This should never happen for GCFv1 instances. + throw new FirebaseError(`Secret ${secret.secret} version is unexpectedly empty.`); + } + if (secret.projectId === projectId || secret.projectId === projectNumber) { + // We already know that secret.version isn't empty, but TS can't figure it out for some reason. + if (secret.version) { + secrets.push({ ...secret, version: secret.version }); + } + } + } + + for (const sev of secrets) { + let name = sev.secret; + if (name.includes("/")) { + const secret = parseSecretResourceName(name); + name = secret.name; + } + + let version = sev.version; + if (version === "latest") { + // We need to figure out what "latest" resolves to. + const resolved = await getSecretVersion(projectId, name, version); + version = resolved.versionId; + } + + prunedSecrets.delete(pruneKey(name, version)); + } + + return Array.from(prunedSecrets) + .map((key) => key.split("@")) + .map(([secret, version]) => ({ projectId, version, secret, key: secret })); +} + +type PruneResult = { + destroyed: backend.SecretEnvVar[]; + erred: { message: string }[]; +}; + +/** + * Prune and destroy all unused secret versions. Only Firebase managed secrets will be scanned. + */ +export async function pruneAndDestroySecrets( + projectInfo: ProjectInfo, + endpoints: backend.Endpoint[], +): Promise { + const { projectId, projectNumber } = projectInfo; + + logger.debug("Pruning secrets to find unused secret versions..."); + const unusedSecrets: Required[] = await module.exports.pruneSecrets( + { projectId, projectNumber }, + endpoints, + ); + + if (unusedSecrets.length === 0) { + return { destroyed: [], erred: [] }; + } + + const destroyed: PruneResult["destroyed"] = []; + const erred: PruneResult["erred"] = []; + const msg = unusedSecrets.map((s) => `${s.secret}@${s.version}`); + logger.debug(`Found unused secret versions: ${msg}. Destroying them...`); + const destroyResults = await utils.allSettled( + unusedSecrets.map(async (sev) => { + await destroySecretVersion(sev.projectId, sev.secret, sev.version); + return sev; + }), + ); + + for (const result of destroyResults) { + if (result.status === "fulfilled") { + destroyed.push(result.value); + } else { + erred.push(result.reason as { message: string }); + } + } + return { destroyed, erred }; +} + +/** + * Updates given endpoint to use the given secret version. + */ +export async function updateEndpointSecret( + projectInfo: ProjectInfo, + secretVersion: SecretVersion, + endpoint: backend.Endpoint, +): Promise { + const { projectId, projectNumber } = projectInfo; + + if (!inUse(projectInfo, secretVersion.secret, endpoint)) { + return endpoint; + } + + const updatedSevs: Required[] = []; + for (const sev of of([endpoint])) { + const updatedSev = { ...sev } as Required; + if ( + (updatedSev.projectId === projectId || updatedSev.projectId === projectNumber) && + updatedSev.secret === secretVersion.secret.name + ) { + updatedSev.version = secretVersion.versionId; + } + updatedSevs.push(updatedSev); + } + + if (endpoint.platform === "gcfv1") { + const fn = gcfV1.functionFromEndpoint(endpoint, ""); + const op = await gcfV1.updateFunction({ + name: fn.name, + runtime: fn.runtime, + entryPoint: fn.entryPoint, + secretEnvironmentVariables: updatedSevs, + }); + const cfn = await poller.pollOperation({ + ...gcfV1PollerOptions, + operationResourceName: op.name, + }); + return gcfV1.endpointFromFunction(cfn); + } else if (endpoint.platform === "gcfv2") { + const fn = gcfV2.functionFromEndpoint(endpoint); + const op = await gcfV2.updateFunction({ + ...fn, + serviceConfig: { + ...fn.serviceConfig, + secretEnvironmentVariables: updatedSevs, + }, + }); + const cfn = await poller.pollOperation({ + ...gcfV2PollerOptions, + operationResourceName: op.name, + }); + return gcfV2.endpointFromFunction(cfn); + } else { + assertExhaustive(endpoint.platform); + } +} + +/** + * Describe the given secret. + */ +export async function describeSecret(key: string, options: Options): Promise { + const projectId = needProjectId(options); + const versions = await listSecretVersions(projectId, key); + + const table = new Table({ + head: ["Version", "State"], + style: { head: ["yellow"] }, + }); + for (const version of versions) { + table.push([version.versionId, version.state]); + } + logger.info(table.toString()); + return { secrets: versions }; +} diff --git a/src/test/functionsConfig.spec.ts b/src/functionsConfig.spec.ts similarity index 95% rename from src/test/functionsConfig.spec.ts rename to src/functionsConfig.spec.ts index 4aa12a4279b..158f4df6723 100644 --- a/src/test/functionsConfig.spec.ts +++ b/src/functionsConfig.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; -import * as functionsConfig from "../functionsConfig"; +import * as functionsConfig from "./functionsConfig"; describe("config.parseSetArgs", () => { it("should throw if a reserved namespace is used", () => { diff --git a/src/functionsConfig.ts b/src/functionsConfig.ts index e33109e46c5..eda054cf627 100644 --- a/src/functionsConfig.ts +++ b/src/functionsConfig.ts @@ -1,7 +1,8 @@ import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; -import * as api from "./api"; +import { firebaseApiOrigin } from "./api"; +import { Client } from "./apiv2"; import { ensure as ensureApiEnabled } from "./ensureApiEnabled"; import { FirebaseError } from "./error"; import { needProjectId } from "./projectUtils"; @@ -10,6 +11,8 @@ import * as args from "./deploy/functions/args"; export const RESERVED_NAMESPACES = ["firebase"]; +const apiClient = new Client({ urlPrefix: firebaseApiOrigin() }); + interface Id { config: string; variable: string; @@ -27,7 +30,7 @@ function setVariable( projectId: string, configId: string, varPath: string, - val: string | object + val: string | object, ): Promise { if (configId === "" || varPath === "") { const msg = "Invalid argument, each config value must have a 2-part key (e.g. foo.bar)."; @@ -37,7 +40,7 @@ function setVariable( } function isReservedNamespace(id: Id) { - return _.some(RESERVED_NAMESPACES, (reserved) => { + return RESERVED_NAMESPACES.some((reserved) => { return id.config.toLowerCase().startsWith(reserved); }); } @@ -55,7 +58,7 @@ export function varNameToIds(varName: string): Id { } export function idsToVarName(projectId: string, configId: string, varId: string): string { - return _.join(["projects", projectId, "configs", configId, "variables", varId], "/"); + return ["projects", projectId, "configs", configId, "variables", varId].join("/"); } // TODO(inlined): Yank and inline into Fabricator @@ -70,10 +73,9 @@ export function getAppEngineLocation(config: any): string { export async function getFirebaseConfig(options: any): Promise { const projectId = needProjectId(options); - const response = await api.request("GET", "/v1beta1/projects/" + projectId + "/adminSdkConfig", { - auth: true, - origin: api.firebaseApiOrigin, - }); + const response = await apiClient.get( + `/v1beta1/projects/${projectId}/adminSdkConfig`, + ); return response.body; } @@ -83,24 +85,24 @@ export async function setVariablesRecursive( projectId: string, configId: string, varPath: string, - val: string | { [key: string]: any } + val: string | { [key: string]: any }, ): Promise { let parsed = val; - if (_.isString(val)) { + if (typeof val === "string") { try { // Only attempt to parse 'val' if it is a String (takes care of unparsed JSON, numbers, quoted string, etc.) parsed = JSON.parse(val); - } catch (e) { + } catch (e: any) { // 'val' is just a String } } // If 'parsed' is object, call again - if (_.isPlainObject(parsed)) { + if (typeof parsed === "object" && parsed !== null) { return Promise.all( - _.map(parsed, (item: any, key: string) => { - const newVarPath = varPath ? _.join([varPath, key], "/") : key; + Object.entries(parsed).map(([key, item]) => { + const newVarPath = varPath ? [varPath, key].join("/") : key; return setVariablesRecursive(projectId, configId, newVarPath, item); - }) + }), ); } @@ -111,16 +113,16 @@ export async function setVariablesRecursive( export async function materializeConfig(configName: string, output: any): Promise { const materializeVariable = async function (varName: string) { const variable = await runtimeconfig.variables.get(varName); - const id = exports.varNameToIds(variable.name); + const id = varNameToIds(variable.name); const key = id.config + "." + id.variable.split("/").join("."); _.set(output, key, variable.text); }; - const traverseVariables = async function (variables: any) { + const traverseVariables = async function (variables: { name: string }[]) { return Promise.all( - _.map(variables, (variable) => { + variables.map((variable) => { return materializeVariable(variable.name); - }) + }), ); }; @@ -129,17 +131,20 @@ export async function materializeConfig(configName: string, output: any): Promis return output; } -export async function materializeAll(projectId: string): Promise<{ [key: string]: any }> { +export async function materializeAll(projectId: string): Promise> { const output = {}; const configs = await runtimeconfig.configs.list(projectId); + if (!Array.isArray(configs) || !configs.length) { + return output; + } await Promise.all( - _.map(configs, (config) => { + configs.map | undefined>((config: any) => { if (config.name.match(new RegExp("configs/firebase"))) { // ignore firebase config return; } - return exports.materializeConfig(config.name, output); - }) + return materializeConfig(config.name, output); + }), ); return output; } @@ -152,7 +157,7 @@ interface ParsedArg { export function parseSetArgs(args: string[]): ParsedArg[] { const parsed: ParsedArg[] = []; - _.forEach(args, (arg) => { + for (const arg of args) { const parts = arg.split("="); const key = parts[0]; if (parts.length < 2) { @@ -173,18 +178,18 @@ export function parseSetArgs(args: string[]): ParsedArg[] { varId: id.variable, val: val, }); - }); + } return parsed; } export function parseUnsetArgs(args: string[]): ParsedArg[] { const parsed: ParsedArg[] = []; let splitArgs: string[] = []; - _.forEach(args, (arg) => { - splitArgs = _.union(splitArgs, arg.split(",")); - }); + for (const arg of args) { + splitArgs = Array.from(new Set([...splitArgs, ...arg.split(",")])); + } - _.forEach(splitArgs, (key) => { + for (const key of splitArgs) { const id = keyToIds(key); if (isReservedNamespace(id)) { throw new FirebaseError("Cannot unset reserved namespace " + clc.bold(id.config)); @@ -194,6 +199,6 @@ export function parseUnsetArgs(args: string[]): ParsedArg[] { configId: id.config, varId: id.variable, }); - }); + } return parsed; } diff --git a/src/functionsConfigClone.js b/src/functionsConfigClone.js deleted file mode 100644 index c4c30174b62..00000000000 --- a/src/functionsConfigClone.js +++ /dev/null @@ -1,88 +0,0 @@ -"use strict"; - -var _ = require("lodash"); - -var clc = require("cli-color"); -var { FirebaseError } = require("./error"); -var functionsConfig = require("./functionsConfig"); -var runtimeconfig = require("./gcp/runtimeconfig"); - -// Tests whether short is a prefix of long -var _matchPrefix = function (short, long) { - if (short.length > long.length) { - return false; - } - return _.reduce( - short, - function (accum, x, i) { - return accum && x === long[i]; - }, - true - ); -}; - -var _applyExcept = function (json, except) { - _.forEach(except, function (key) { - _.unset(json, key); - }); -}; - -var _cloneVariable = function (varName, toProject) { - return runtimeconfig.variables.get(varName).then(function (variable) { - var id = functionsConfig.varNameToIds(variable.name); - return runtimeconfig.variables.set(toProject, id.config, id.variable, variable.text); - }); -}; - -var _cloneConfig = function (configName, toProject) { - return runtimeconfig.variables.list(configName).then(function (variables) { - return Promise.all( - _.map(variables, function (variable) { - return _cloneVariable(variable.name, toProject); - }) - ); - }); -}; - -var _cloneConfigOrVariable = function (key, fromProject, toProject) { - var parts = key.split("."); - if (_.includes(functionsConfig.RESERVED_NAMESPACES, parts[0])) { - throw new FirebaseError("Cannot clone reserved namespace " + clc.bold(parts[0])); - } - var configName = _.join(["projects", fromProject, "configs", parts[0]], "/"); - if (parts.length === 1) { - return _cloneConfig(configName, toProject); - } - return runtimeconfig.variables.list(configName).then(function (variables) { - var promises = []; - _.forEach(variables, function (variable) { - var varId = functionsConfig.varNameToIds(variable.name).variable; - var variablePrefixFilter = parts.slice(1); - if (_matchPrefix(variablePrefixFilter, varId.split("/"))) { - promises.push(_cloneVariable(variable.name, toProject)); - } - }); - return Promise.all(promises); - }); -}; - -module.exports = function (fromProject, toProject, only, except) { - except = except || []; - - if (only) { - return Promise.all( - _.map(only, function (key) { - return _cloneConfigOrVariable(key, fromProject, toProject); - }) - ); - } - return functionsConfig.materializeAll(fromProject).then(function (toClone) { - _.unset(toClone, "firebase"); // Do not clone firebase config - _applyExcept(toClone, except); - return Promise.all( - _.map(toClone, function (val, configId) { - return functionsConfig.setVariablesRecursive(toProject, configId, "", val); - }) - ); - }); -}; diff --git a/src/functionsConfigClone.ts b/src/functionsConfigClone.ts new file mode 100644 index 00000000000..289312f5c19 --- /dev/null +++ b/src/functionsConfigClone.ts @@ -0,0 +1,83 @@ +import * as _ from "lodash"; +import * as clc from "colorette"; + +import { FirebaseError } from "./error"; +import * as functionsConfig from "./functionsConfig"; +import * as runtimeconfig from "./gcp/runtimeconfig"; + +// Tests whether short is a prefix of long +function matchPrefix(short: any[], long: any[]): boolean { + if (short.length > long.length) { + return false; + } + return short.reduce((accum, x, i) => accum && x === long[i], true); +} + +function applyExcept(json: any, except: any[]) { + for (const key of except) { + _.unset(json, key); + } +} + +function cloneVariable(varName: string, toProject: any): Promise { + return runtimeconfig.variables.get(varName).then((variable) => { + const id = functionsConfig.varNameToIds(variable.name); + return runtimeconfig.variables.set(toProject, id.config, id.variable, variable.text); + }); +} + +function cloneConfig(configName: string, toProject: any): Promise { + return runtimeconfig.variables.list(configName).then((variables) => { + return Promise.all( + variables.map((variable: { name: string }) => { + return cloneVariable(variable.name, toProject); + }), + ); + }); +} + +async function cloneConfigOrVariable(key: string, fromProject: any, toProject: any): Promise { + const parts = key.split("."); + if (functionsConfig.RESERVED_NAMESPACES.includes(parts[0])) { + throw new FirebaseError("Cannot clone reserved namespace " + clc.bold(parts[0])); + } + const configName = ["projects", fromProject, "configs", parts[0]].join("/"); + if (parts.length === 1) { + return cloneConfig(configName, toProject); + } + return runtimeconfig.variables.list(configName).then((variables) => { + const promises: Promise[] = []; + for (const variable of variables) { + const varId = functionsConfig.varNameToIds(variable.name).variable; + const variablePrefixFilter = parts.slice(1); + if (matchPrefix(variablePrefixFilter, varId.split("/"))) { + promises.push(cloneVariable(variable.name, toProject)); + } + } + return Promise.all(promises); + }); +} + +export async function functionsConfigClone( + fromProject: any, + toProject: any, + only: string[] | undefined, + except: string[] = [], +): Promise { + if (only) { + return Promise.all( + only.map((key) => { + return cloneConfigOrVariable(key, fromProject, toProject); + }), + ); + } + return functionsConfig.materializeAll(fromProject).then((toClone) => { + _.unset(toClone, "firebase"); // Do not clone firebase config + applyExcept(toClone, except); + return Promise.all( + Object.entries(toClone).map(([configId, val]) => { + return functionsConfig.setVariablesRecursive(toProject, configId, "", val); + }), + ); + }); +} diff --git a/src/functionsShellCommandAction.ts b/src/functionsShellCommandAction.ts index 7f7dd859857..b4768bc802d 100644 --- a/src/functionsShellCommandAction.ts +++ b/src/functionsShellCommandAction.ts @@ -1,21 +1,22 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as repl from "repl"; import * as _ from "lodash"; -import * as request from "request"; import * as util from "util"; +import * as shell from "./emulator/functionsEmulatorShell"; +import * as commandUtils from "./emulator/commandUtils"; import { FunctionsServer } from "./serve/functions"; -import * as LocalFunction from "./localFunction"; +import LocalFunction from "./localFunction"; import * as utils from "./utils"; import { logger } from "./logger"; -import * as shell from "./emulator/functionsEmulatorShell"; -import * as commandUtils from "./emulator/commandUtils"; import { EMULATORS_SUPPORTED_BY_FUNCTIONS, EmulatorInfo, Emulators } from "./emulator/types"; import { EmulatorHubClient } from "./emulator/hubClient"; +import { resolveHostAndAssignPorts } from "./emulator/portUtils"; import { Constants } from "./emulator/constants"; -import { findAvailablePort } from "./emulator/portUtils"; import { Options } from "./options"; +import { HTTPS_SENTINEL } from "./localFunction"; +import { needProjectId } from "./projectUtils"; const serveFunctions = new FunctionsServer(); @@ -29,8 +30,8 @@ export const actionFunction = async (options: Options) => { debugPort = commandUtils.parseInspectionPort(options); } - utils.assertDefined(options.project); - const hubClient = new EmulatorHubClient(options.project); + needProjectId(options); + const hubClient = new EmulatorHubClient(options.project!); let remoteEmulators: Record = {}; if (hubClient.foundHub()) { @@ -39,33 +40,46 @@ export const actionFunction = async (options: Options) => { } const runningEmulators = EMULATORS_SUPPORTED_BY_FUNCTIONS.filter( - (e) => remoteEmulators[e] !== undefined + (e) => remoteEmulators[e] !== undefined, ); const otherEmulators = EMULATORS_SUPPORTED_BY_FUNCTIONS.filter( - (e) => remoteEmulators[e] === undefined + (e) => remoteEmulators[e] === undefined, ); + let host = Constants.getDefaultHost(); + // If the port was not set by the --port flag or determined from 'firebase.json', just scan + // up from 5000 + let port = 5000; + if (typeof options.port === "number") { + port = options.port; + } + const functionsInfo = remoteEmulators[Emulators.FUNCTIONS]; if (functionsInfo) { utils.logLabeledWarning( "functions", - `You are already running the Cloud Functions emulator on port ${functionsInfo.port}. Running the emulator and the Functions shell simultaenously can result in unexpected behavior.` + `You are already running the Cloud Functions emulator on port ${functionsInfo.port}. Running the emulator and the Functions shell simultaenously can result in unexpected behavior.`, ); } else if (!options.port) { // If the user did not pass in any port and the functions emulator is not already running, we can // use the port defined for the Functions emulator in their firebase.json - options.port = options.config.src.emulators?.functions?.port; + port = options.config.src.emulators?.functions?.port ?? port; + host = options.config.src.emulators?.functions?.host ?? host; + options.host = host; } - // If the port was not set by the --port flag or determined from 'firebase.json', just scan - // up from 5000 - if (!options.port) { - options.port = await findAvailablePort("localhost", 5000); - } + const listen = ( + await resolveHostAndAssignPorts({ + [Emulators.FUNCTIONS]: { host, port }, + }) + ).functions; + // TODO: Listen on secondary addresses. + options.host = listen[0].address; + options.port = listen[0].port; return serveFunctions .start(options, { - quiet: true, + verbosity: "QUIET", remoteEmulators, debugPort, }) @@ -86,7 +100,7 @@ export const actionFunction = async (options: Options) => { if (emulator.emulatedFunctions.includes(trigger.id)) { const localFunction = new LocalFunction(trigger, emulator.urls, emulator); const triggerNameDotNotation = trigger.name.replace(/-/g, "."); - _.set(context, triggerNameDotNotation, localFunction.call); + _.set(context, triggerNameDotNotation, localFunction.makeFn()); } } context.help = @@ -100,21 +114,19 @@ export const actionFunction = async (options: Options) => { "functions", `Connected to running ${clc.bold(e)} emulator at ${info.host}:${ info.port - }, calls to this service will affect the emulator` + }, calls to this service will affect the emulator`, ); } utils.logLabeledWarning( "functions", `The following emulators are not running, calls to these services will affect production: ${clc.bold( - otherEmulators.join(", ") - )}` + otherEmulators.join(", "), + )}`, ); const writer = (output: any) => { - // Prevent full print out of Request object when a request is made - // @ts-ignore - if (output instanceof request.Request) { - return "Sent request to function."; + if (output === HTTPS_SENTINEL) { + return HTTPS_SENTINEL; } return util.inspect(output); }; diff --git a/src/gcp/apphosting.spec.ts b/src/gcp/apphosting.spec.ts new file mode 100644 index 00000000000..f2d4ab97184 --- /dev/null +++ b/src/gcp/apphosting.spec.ts @@ -0,0 +1,282 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as apphosting from "./apphosting"; + +describe("apphosting", () => { + describe("getNextBuildId", () => { + let listRollouts: sinon.SinonStub; + let listBuilds: sinon.SinonStub; + + beforeEach(() => { + listRollouts = sinon.stub(apphosting, "listRollouts"); + listBuilds = sinon.stub(apphosting, "listBuilds"); + }); + + afterEach(() => { + listRollouts.restore(); + listBuilds.restore(); + }); + + function idPrefix(date: Date): string { + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, "0"); + const day = String(date.getUTCDate()).padStart(2, "0"); + return `build-${year}-${month}-${day}`; + } + + it("should handle explicit counters", async () => { + const id = await apphosting.getNextRolloutId("unused", "unused", "unused", 1); + expect(id).matches(new RegExp(`^${idPrefix(new Date())}-001$`)); + expect(listRollouts).to.not.have.been.called; + }); + + it("should handle missing regions (rollouts)", async () => { + listRollouts.resolves({ + rollouts: [], + unreachable: ["us-central1"], + }); + listBuilds.resolves({ + builds: [], + unreachable: [], + }); + + await expect( + apphosting.getNextRolloutId("project", "us-central1", "backend"), + ).to.be.rejectedWith(/unreachable .*us-central1/); + expect(listRollouts).to.have.been.calledWith("project", "us-central1", "backend"); + }); + + it("should handle missing regions (builds)", async () => { + listRollouts.resolves({ + rollouts: [], + unreachable: [], + }); + listBuilds.resolves({ + builds: [], + unreachable: ["us-central1"], + }); + + await expect( + apphosting.getNextRolloutId("project", "us-central1", "backend"), + ).to.be.rejectedWith(/unreachable .*us-central1/); + expect(listRollouts).to.have.been.calledWith("project", "us-central1", "backend"); + }); + + it("should handle the first build of a day", async () => { + listRollouts.resolves({ + rollouts: [], + unreachable: [], + }); + listBuilds.resolves({ + builds: [], + unreachable: [], + }); + + const id = await apphosting.getNextRolloutId("project", "location", "backend"); + expect(id).equals(`${idPrefix(new Date())}-001`); + expect(listRollouts).to.have.been.calledWith("project", "location", "backend"); + }); + + it("should increment from the correct date", async () => { + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + + listRollouts.resolves({ + rollouts: [ + { + name: `projects/project/locations/location/backends/backend/rollouts/${idPrefix(yesterday)}-005`, + }, + { + name: `projects/project/locations/location/backends/backend/rollouts/${idPrefix(today)}-001`, + }, + ], + unreachable: [], + }); + listBuilds.resolves({ + builds: [ + { + name: `projects/project/locations/location/backends/backend/builds/${idPrefix(yesterday)}-005`, + }, + { + name: `projects/project/locations/location/backends/backend/builds/${idPrefix(today)}-001`, + }, + ], + unreachable: [], + }); + + const id = await apphosting.getNextRolloutId("project", "location", "backend"); + expect(id).to.equal(`${idPrefix(today)}-002`); + }); + + it("should handle the first build of the day", async () => { + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + + listRollouts.resolves({ + rollouts: [ + { + name: `projects/project/locations/location/backends/backend/rollouts/${idPrefix(yesterday)}-005`, + }, + ], + unreachable: [], + }); + listBuilds.resolves({ + builds: [ + { + name: `projects/project/locations/location/backends/backend/builds/${idPrefix(yesterday)}-005`, + }, + ], + unreachable: [], + }); + + const id = await apphosting.getNextRolloutId("project", "location", "backend"); + expect(id).to.equal(`${idPrefix(today)}-001`); + }); + + it("should handle build & rollout names out of sync (build is latest)", async () => { + const today = new Date(); + listRollouts.resolves({ + rollouts: [ + { + name: `projects/project/locations/location/backends/backend/rollouts/${idPrefix(today)}-001`, + }, + ], + }); + listBuilds.resolves({ + builds: [ + { + name: `projects/project/locations/location/backends/backend/builds/${idPrefix(today)}-002`, + }, + ], + }); + + const id = await apphosting.getNextRolloutId("project", "location", "backend"); + expect(id).to.equal(`${idPrefix(today)}-003`); + }); + + it("should handle build & rollout names out of sync (rollout is latest)", async () => { + const today = new Date(); + listRollouts.resolves({ + rollouts: [ + { + name: `projects/project/locations/location/backends/backend/rollouts/${idPrefix(today)}-002`, + }, + ], + }); + listBuilds.resolves({ + builds: [ + { + name: `projects/project/locations/location/backends/backend/builds/${idPrefix(today)}-001`, + }, + ], + }); + + const id = await apphosting.getNextRolloutId("project", "location", "backend"); + expect(id).to.equal(`${idPrefix(today)}-003`); + }); + }); + + describe("list APIs", () => { + let get: sinon.SinonStub; + + beforeEach(() => { + get = sinon.stub(apphosting.client, "get"); + }); + + afterEach(() => { + get.restore(); + }); + + it("paginates listBackends", async () => { + get.onFirstCall().resolves({ + body: { + backends: [ + { + name: "abc", + }, + ], + nextPageToken: "2", + }, + }); + get.onSecondCall().resolves({ + body: { + unreachable: ["us-central1"], + }, + }); + await expect(apphosting.listBackends("p", "l")).to.eventually.deep.equal({ + backends: [ + { + name: "abc", + }, + ], + unreachable: ["us-central1"], + }); + expect(get).to.have.been.calledTwice; + expect(get).to.have.been.calledWithMatch("projects/p/locations/l/backends", { + queryParams: { pageToken: "2" }, + }); + }); + + it("paginates listBuilds", async () => { + get.onFirstCall().resolves({ + body: { + builds: [ + { + name: "abc", + }, + ], + nextPageToken: "2", + }, + }); + get.onSecondCall().resolves({ + body: { + unreachable: ["us-central1"], + }, + }); + await expect(apphosting.listBuilds("p", "l", "b")).to.eventually.deep.equal({ + builds: [ + { + name: "abc", + }, + ], + unreachable: ["us-central1"], + }); + expect(get).to.have.been.calledTwice; + expect(get).to.have.been.calledWithMatch("projects/p/locations/l/backends/b/builds", { + queryParams: { pageToken: "2" }, + }); + }); + + it("paginates listRollouts", async () => { + get.onFirstCall().resolves({ + body: { + rollouts: [ + { + name: "abc", + }, + ], + nextPageToken: "2", + }, + }); + get.onSecondCall().resolves({ + body: { + unreachable: ["us-central1"], + }, + }); + await expect(apphosting.listRollouts("p", "l", "b")).to.eventually.deep.equal({ + rollouts: [ + { + name: "abc", + }, + ], + unreachable: ["us-central1"], + }); + expect(get).to.have.been.calledTwice; + expect(get).to.have.been.calledWithMatch("projects/p/locations/l/backends/b/rollouts", { + queryParams: { pageToken: "2" }, + }); + }); + }); +}); diff --git a/src/gcp/apphosting.ts b/src/gcp/apphosting.ts new file mode 100644 index 00000000000..871d46f65d4 --- /dev/null +++ b/src/gcp/apphosting.ts @@ -0,0 +1,602 @@ +import * as proto from "../gcp/proto"; +import { Client } from "../apiv2"; +import { needProjectId } from "../projectUtils"; +import { apphostingOrigin, apphostingP4SADomain } from "../api"; +import { ensure } from "../ensureApiEnabled"; +import * as deploymentTool from "../deploymentTool"; +import { FirebaseError } from "../error"; +import { DeepOmit, RecursiveKeyOf, assertImplements } from "../metaprogramming"; + +export const API_VERSION = "v1alpha"; + +export const client = new Client({ + urlPrefix: apphostingOrigin(), + auth: true, + apiVersion: API_VERSION, +}); + +type BuildState = "BUILDING" | "BUILD" | "DEPLOYING" | "READY" | "FAILED"; + +interface Codebase { + repository?: string; + rootDirectory: string; +} + +/** + * Specifies how Backend's data is replicated and served. + * GLOBAL_ACCESS: Stores and serves content from multiple points-of-presence (POP) + * REGIONAL_STRICT: Restricts data and serving infrastructure in Backend's location + * + */ +export type ServingLocality = "GLOBAL_ACCESS" | "REGIONAL_STRICT"; + +/** A Backend, the primary resource of Frameworks. */ +export interface Backend { + name: string; + mode?: string; + codebase: Codebase; + servingLocality: ServingLocality; + labels: Record; + createTime: string; + updateTime: string; + uri: string; + serviceAccount?: string; + appId?: string; +} + +export type BackendOutputOnlyFields = "name" | "createTime" | "updateTime" | "uri"; + +assertImplements>(); + +export interface Build { + name: string; + state: BuildState; + error: Status; + image: string; + config?: BuildConfig; + source: BuildSource; + sourceRef: string; + buildLogsUri?: string; + displayName?: string; + labels?: Record; + annotations?: Record; + uuid: string; + etag: string; + reconciling: boolean; + createTime: string; + updateTime: string; + deleteTime: string; +} + +export interface ListBuildsResponse { + builds: Build[]; + nextPageToken?: string; + unreachable?: string[]; +} + +export type BuildOutputOnlyFields = + | "state" + | "error" + | "image" + | "sourceRef" + | "buildLogsUri" + | "reconciling" + | "uuid" + | "etag" + | "createTime" + | "updateTime" + | "deleteTime" + | "source.codebase.displayName" + | "source.codebase.hash" + | "source.codebase.commitMessage" + | "source.codebase.uri" + | "source.codebase.commitTime"; + +assertImplements>(); + +export interface BuildConfig { + minInstances?: number; + memory?: string; +} + +interface BuildSource { + codebase: CodebaseSource; +} + +interface CodebaseSource { + // oneof reference + branch?: string; + commit?: string; + tag?: string; + // end oneof reference + displayName: string; + hash: string; + commitMessage: string; + uri: string; + commitTime: string; +} + +interface Status { + code: number; + message: string; + details: unknown; +} + +type RolloutState = + | "STATE_UNSPECIFIED" + | "QUEUED" + | "PENDING_BUILD" + | "PROGRESSING" + | "PAUSED" + | "SUCCEEDED" + | "FAILED" + | "CANCELLED"; + +export interface Rollout { + name: string; + state: RolloutState; + paused?: boolean; + pauseTime: string; + error?: Error; + build: string; + stages?: RolloutStage[]; + displayName?: string; + createTime: string; + updateTime: string; + deleteTime?: string; + purgeTime?: string; + labels?: Record; + annotations?: Record; + uid: string; + etag: string; + reconciling: boolean; +} + +export type RolloutOutputOnlyFields = + | "state" + | "pauseTime" + | "createTime" + | "updateTime" + | "deleteTime" + | "purgeTime" + | "uid" + | "etag" + | "reconciling"; + +assertImplements>(); + +export interface ListRolloutsResponse { + rollouts: Rollout[]; + unreachable: string[]; + nextPageToken?: string; +} + +export interface Traffic { + name: string; + // oneof traffic_management + target?: TrafficSet; + rolloutPolicy?: RolloutPolicy; + // end oneof traffic_management + current: TrafficSet; + reconciling: boolean; + createTime: string; + updateTime: string; + annotations?: Record; + etag: string; + uid: string; +} + +export type TrafficOutputOnlyFields = + | "current" + | "reconciling" + | "createTime" + | "updateTime" + | "etag" + | "uid" + | "rolloutPolicy.disabledTime" + | "rolloutPolicy.stages.startTime" + | "rolloutPolicy.stages.endTime"; + +assertImplements>(); + +export interface TrafficSet { + splits: TrafficSplit[]; +} + +export interface TrafficSplit { + build: string; + percent: number; +} + +export interface RolloutPolicy { + // oneof trigger + codebaseBranch?: string; + codebaseTagPattern?: string; + // end oneof trigger + stages?: RolloutStage[]; + disabled?: boolean; + + // TODO: This will be undefined if disabled is not true, right? + disabledTime: string; +} + +export type RolloutProgression = + | "PROGRESSION_UNSPECIFIED" + | "IMMEDIATE" + | "LINEAR" + | "EXPONENTIAL" + | "PAUSE"; + +export interface RolloutStage { + progression: RolloutProgression; + duration?: { + seconds: number; + nanos: number; + }; + targetPercent?: number; + startTime: string; + endTime: string; +} + +interface OperationMetadata { + createTime: string; + endTime: string; + target: string; + verb: string; + statusDetail: string; + cancelRequested: boolean; + apiVersion: string; +} + +export interface Operation { + name: string; + metadata?: OperationMetadata; + done: boolean; + // oneof result + error?: Status; + response?: any; + // end oneof result +} + +export interface ListBackendsResponse { + backends: Backend[]; + nextPageToken?: string; + unreachable: string[]; +} + +const P4SA_DOMAIN = apphostingP4SADomain(); + +/** + * Returns the App Hosting service agent. + */ +export function serviceAgentEmail(projectNumber: string): string { + return `service-${projectNumber}@${P4SA_DOMAIN}`; +} + +/** Splits a backend resource name into its parts. */ +export function parseBackendName(backendName: string): { + projectName: string; + location: string; + id: string; +} { + // sample value: "projects//locations/us-central1/backends/" + const [, projectName, , location, , id] = backendName.split("/"); + return { projectName, location, id }; +} + +/** + * Creates a new Backend in a given project and location. + */ +export async function createBackend( + projectId: string, + location: string, + backendReqBoby: DeepOmit, + backendId: string, +): Promise { + const res = await client.post, Operation>( + `projects/${projectId}/locations/${location}/backends`, + { + ...backendReqBoby, + labels: { + ...backendReqBoby.labels, + ...deploymentTool.labels(), + }, + }, + { queryParams: { backendId } }, + ); + + return res.body; +} + +/** + * Gets backend details. + */ +export async function getBackend( + projectId: string, + location: string, + backendId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}`; + const res = await client.get(name); + return res.body; +} + +/** + * List all backends present in a project and location. + */ +export async function listBackends( + projectId: string, + location: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends`; + let pageToken: string | undefined; + const res: ListBackendsResponse = { + backends: [], + unreachable: [], + }; + + do { + const queryParams: Record = pageToken ? { pageToken } : {}; + const int = await client.get(name, { queryParams }); + res.backends.push(...(int.body.backends || [])); + res.unreachable?.push(...(int.body.unreachable || [])); + pageToken = int.body.nextPageToken; + } while (pageToken); + + res.unreachable = [...new Set(res.unreachable)]; + return res; +} + +/** + * Deletes a backend with backendId in a given project and location. + */ +export async function deleteBackend( + projectId: string, + location: string, + backendId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}`; + const res = await client.delete(name, { queryParams: { force: "true" } }); + + return res.body; +} + +/** + * Get a Build by Id + */ +export async function getBuild( + projectId: string, + location: string, + backendId: string, + buildId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`; + const res = await client.get(name); + return res.body; +} + +/** + * List Builds by backend + */ +export async function listBuilds( + projectId: string, + location: string, + backendId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/builds`; + let pageToken: string | undefined; + const res: ListBuildsResponse = { + builds: [], + unreachable: [], + }; + + do { + const queryParams: Record = pageToken ? { pageToken } : {}; + const int = await client.get(name, { queryParams }); + res.builds.push(...(int.body.builds || [])); + res.unreachable?.push(...(int.body.unreachable || [])); + pageToken = int.body.nextPageToken; + } while (pageToken); + + res.unreachable = [...new Set(res.unreachable)]; + return res; +} + +/** + * Creates a new Build in a given project and location. + */ +export async function createBuild( + projectId: string, + location: string, + backendId: string, + buildId: string, + buildInput: DeepOmit, +): Promise { + const res = await client.post, Operation>( + `projects/${projectId}/locations/${location}/backends/${backendId}/builds`, + { + ...buildInput, + labels: { + ...buildInput.labels, + ...deploymentTool.labels(), + }, + }, + { queryParams: { buildId } }, + ); + return res.body; +} + +/** + * Create a new rollout for a backend. + */ +export async function createRollout( + projectId: string, + location: string, + backendId: string, + rolloutId: string, + rollout: DeepOmit, + validateOnly = false, +): Promise { + const res = await client.post, Operation>( + `projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`, + { + ...rollout, + labels: { + ...rollout.labels, + ...deploymentTool.labels(), + }, + }, + { queryParams: { rolloutId, validateOnly: validateOnly ? "true" : "false" } }, + ); + return res.body; +} + +/** + * List all rollouts for a backend. + */ +export async function listRollouts( + projectId: string, + location: string, + backendId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`; + let pageToken: string | undefined = undefined; + const res: ListRolloutsResponse = { + rollouts: [], + unreachable: [], + }; + + do { + const queryParams: Record = pageToken ? { pageToken } : {}; + const int = await client.get(name, { queryParams }); + res.rollouts.splice(res.rollouts.length, 0, ...(int.body.rollouts || [])); + res.unreachable.splice(res.unreachable.length, 0, ...(int.body.unreachable || [])); + pageToken = int.body.nextPageToken; + } while (pageToken); + + res.unreachable = [...new Set(res.unreachable)]; + return res; +} + +/** + * Update traffic of a backend. + */ +export async function updateTraffic( + projectId: string, + location: string, + backendId: string, + traffic: DeepOmit, +): Promise { + // BUG(b/322891558): setting deep fields on rolloutPolicy doesn't work for some + // reason. Prevent recursion into that field. + const fieldMasks = proto.fieldMasks(traffic, "rolloutPolicy"); + const queryParams = { + updateMask: fieldMasks.join(","), + }; + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`; + const res = await client.patch, Operation>( + name, + { ...traffic, name }, + { + queryParams, + }, + ); + return res.body; +} + +export interface Location { + name: string; + locationId: string; +} + +interface ListLocationsResponse { + locations: Location[]; + nextPageToken?: string; +} + +/** + * Lists information about the supported locations. + */ +export async function listLocations(projectId: string): Promise { + let pageToken: string | undefined = undefined; + let locations: Location[] = []; + do { + const queryParams: Record = pageToken ? { pageToken } : {}; + const response = await client.get(`projects/${projectId}/locations`, { + queryParams, + }); + if (response.body.locations && response.body.locations.length > 0) { + locations = locations.concat(response.body.locations); + } + pageToken = response.body.nextPageToken; + } while (pageToken); + return locations; +} + +/** + * Ensure that the App Hosting API is enabled on the project. + */ +export async function ensureApiEnabled(options: any): Promise { + const projectId = needProjectId(options); + return await ensure(projectId, apphostingOrigin(), "app hosting", true); +} + +/** + * Generates the next build ID to fit with the naming scheme of the backend API. + * @param counter Overrides the counter to use, avoiding an API call. + */ +export async function getNextRolloutId( + projectId: string, + location: string, + backendId: string, + counter?: number, +): Promise { + const date = new Date(); + const year = date.getUTCFullYear(); + // Note: month is 0 based in JS + const month = String(date.getUTCMonth() + 1).padStart(2, "0"); + const day = String(date.getUTCDate()).padStart(2, "0"); + + if (counter) { + return `build-${year}-${month}-${day}-${String(counter).padStart(3, "0")}`; + } + + // Note: must use exports here so that listRollouts can be stubbed in tests. + const rolloutsPromise = (exports as { listRollouts: typeof listRollouts }).listRollouts( + projectId, + location, + backendId, + ); + const buildsPromise = (exports as { listBuilds: typeof listBuilds }).listBuilds( + projectId, + location, + backendId, + ); + const [rollouts, builds] = await Promise.all([rolloutsPromise, buildsPromise]); + + if (builds.unreachable?.includes(location) || rollouts.unreachable?.includes(location)) { + throw new FirebaseError( + `Firebase App Hosting is currently unreachable in location ${location}`, + ); + } + + const test = new RegExp( + `projects/${projectId}/locations/${location}/backends/${backendId}/(rollouts|builds)/build-${year}-${month}-${day}-(\\d+)`, + ); + const highestId = (input: Array<{ name: string }>): number => { + let highest = 0; + for (const i of input) { + const match = i.name.match(test); + if (!match) { + continue; + } + const n = Number(match[2]); + if (n > highest) { + highest = n; + } + } + return highest; + }; + const highest = Math.max(highestId(builds.builds), highestId(rollouts.rollouts)); + return `build-${year}-${month}-${day}-${String(highest + 1).padStart(3, "0")}`; +} diff --git a/src/gcp/artifactregistry.ts b/src/gcp/artifactregistry.ts index e4b5050776e..447811e5f48 100644 --- a/src/gcp/artifactregistry.ts +++ b/src/gcp/artifactregistry.ts @@ -4,7 +4,7 @@ import { artifactRegistryDomain } from "../api"; export const API_VERSION = "v1beta2"; const client = new Client({ - urlPrefix: artifactRegistryDomain, + urlPrefix: artifactRegistryDomain(), auth: true, apiVersion: API_VERSION, }); diff --git a/src/gcp/auth.ts b/src/gcp/auth.ts index bd20fa84ad5..f0480a7ec6e 100644 --- a/src/gcp/auth.ts +++ b/src/gcp/auth.ts @@ -1,7 +1,7 @@ import { Client } from "../apiv2"; import { identityOrigin } from "../api"; -const apiClient = new Client({ urlPrefix: identityOrigin, auth: true }); +const apiClient = new Client({ urlPrefix: identityOrigin(), auth: true }); /** * Returns the list of authorized domains. @@ -10,7 +10,8 @@ const apiClient = new Client({ urlPrefix: identityOrigin, auth: true }); */ export async function getAuthDomains(project: string): Promise { const res = await apiClient.get<{ authorizedDomains: string[] }>( - `/admin/v2/projects/${project}/config` + `/admin/v2/projects/${project}/config`, + { headers: { "x-goog-user-project": project } }, ); return res.body.authorizedDomains; } @@ -28,7 +29,10 @@ export async function updateAuthDomains(project: string, authDomains: string[]): >( `/admin/v2/projects/${project}/config`, { authorizedDomains: authDomains }, - { queryParams: { update_mask: "authorizedDomains" } } + { + queryParams: { update_mask: "authorizedDomains" }, + headers: { "x-goog-user-project": project }, + }, ); return res.body.authorizedDomains; } diff --git a/src/gcp/cloudbilling.ts b/src/gcp/cloudbilling.ts index 6bfe67b909d..36c39bb7d1c 100644 --- a/src/gcp/cloudbilling.ts +++ b/src/gcp/cloudbilling.ts @@ -1,7 +1,9 @@ -import * as api from "../api"; +import { cloudbillingOrigin } from "../api"; +import { Client } from "../apiv2"; import * as utils from "../utils"; const API_VERSION = "v1"; +const client = new Client({ urlPrefix: cloudbillingOrigin(), apiVersion: API_VERSION }); export interface BillingAccount { name: string; @@ -14,14 +16,9 @@ export interface BillingAccount { * @param projectId */ export async function checkBillingEnabled(projectId: string): Promise { - const res = await api.request( - "GET", - utils.endpoint([API_VERSION, "projects", projectId, "billingInfo"]), - { - auth: true, - origin: api.cloudbillingOrigin, - retryCodes: [500, 503], - } + const res = await client.get<{ billingEnabled: boolean }>( + utils.endpoint(["projects", projectId, "billingInfo"]), + { retryCodes: [500, 503] }, ); return res.body.billingEnabled; } @@ -33,19 +30,14 @@ export async function checkBillingEnabled(projectId: string): Promise { */ export async function setBillingAccount( projectId: string, - billingAccountName: string + billingAccountName: string, ): Promise { - const res = await api.request( - "PUT", - utils.endpoint([API_VERSION, "projects", projectId, "billingInfo"]), + const res = await client.put<{ billingAccountName: string }, { billingEnabled: boolean }>( + utils.endpoint(["projects", projectId, "billingInfo"]), { - auth: true, - origin: api.cloudbillingOrigin, - retryCodes: [500, 503], - data: { - billingAccountName: billingAccountName, - }, - } + billingAccountName: billingAccountName, + }, + { retryCodes: [500, 503] }, ); return res.body.billingEnabled; } @@ -55,10 +47,9 @@ export async function setBillingAccount( * @return {!Promise} */ export async function listBillingAccounts(): Promise { - const res = await api.request("GET", utils.endpoint([API_VERSION, "billingAccounts"]), { - auth: true, - origin: api.cloudbillingOrigin, - retryCodes: [500, 503], - }); + const res = await client.get<{ billingAccounts: BillingAccount[] }>( + utils.endpoint(["billingAccounts"]), + { retryCodes: [500, 503] }, + ); return res.body.billingAccounts || []; } diff --git a/src/gcp/cloudbuild.ts b/src/gcp/cloudbuild.ts new file mode 100644 index 00000000000..47a90656565 --- /dev/null +++ b/src/gcp/cloudbuild.ts @@ -0,0 +1,234 @@ +import { Client } from "../apiv2"; +import { cloudbuildOrigin } from "../api"; + +const PAGE_SIZE_MAX = 100; + +const client = new Client({ + urlPrefix: cloudbuildOrigin(), + auth: true, + apiVersion: "v2", +}); + +export interface OperationMetadata { + createTime: string; + endTime: string; + target: string; + verb: string; + requestedCancellation: boolean; + apiVersion: string; +} + +export interface Operation { + name: string; + metadata?: OperationMetadata; + done: boolean; + error?: { code: number; message: string; details: unknown }; + response?: any; +} + +export interface GitHubConfig { + authorizerCredential?: { + oauthTokenSecretVersion: string; + username: string; + }; + appInstallationId?: string; +} + +type InstallationStage = + | "STAGE_UNSPECIFIED" + | "PENDING_CREATE_APP" + | "PENDING_USER_OAUTH" + | "PENDING_INSTALL_APP" + | "COMPLETE"; + +type ConnectionOutputOnlyFields = "createTime" | "updateTime" | "installationState" | "reconciling"; + +export interface Connection { + name: string; + disabled?: boolean; + annotations?: { + [key: string]: string; + }; + etag?: string; + githubConfig?: GitHubConfig; + createTime: string; + updateTime: string; + installationState: { + stage: InstallationStage; + message: string; + actionUri: string; + }; + reconciling: boolean; +} + +type RepositoryOutputOnlyFields = "createTime" | "updateTime"; + +export interface Repository { + name: string; + remoteUri: string; + annotations?: { + [key: string]: string; + }; + etag?: string; + createTime: string; + updateTime: string; +} + +interface LinkableRepositories { + repositories: Repository[]; + nextPageToken: string; +} + +/** + * Creates a Cloud Build V2 Connection. + */ +export async function createConnection( + projectId: string, + location: string, + connectionId: string, + githubConfig: GitHubConfig = {}, +): Promise { + const res = await client.post< + Omit, ConnectionOutputOnlyFields>, + Operation + >( + `projects/${projectId}/locations/${location}/connections`, + { githubConfig }, + { queryParams: { connectionId } }, + ); + return res.body; +} + +/** + * Gets metadata for a Cloud Build V2 Connection. + */ +export async function getConnection( + projectId: string, + location: string, + connectionId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/connections/${connectionId}`; + const res = await client.get(name); + return res.body; +} + +/** + * List metadata for a Cloud Build V2 Connection. + */ +export async function listConnections(projectId: string, location: string): Promise { + const conns: Connection[] = []; + const getNextPage = async (pageToken = ""): Promise => { + const res = await client.get<{ + connections: Connection[]; + nextPageToken?: string; + }>(`/projects/${projectId}/locations/${location}/connections`, { + queryParams: { + pageSize: PAGE_SIZE_MAX, + pageToken, + }, + }); + if (Array.isArray(res.body.connections)) { + conns.push(...res.body.connections); + } + if (res.body.nextPageToken) { + await getNextPage(res.body.nextPageToken); + } + }; + await getNextPage(); + return conns; +} + +/** + * Deletes a Cloud Build V2 Connection. + */ +export async function deleteConnection( + projectId: string, + location: string, + connectionId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/connections/${connectionId}`; + const res = await client.delete(name); + return res.body; +} + +/** + * Gets a list of repositories that can be added to the provided Connection. + */ +export async function fetchLinkableRepositories( + projectId: string, + location: string, + connectionId: string, + pageToken = "", + pageSize = 1000, +): Promise { + const name = `projects/${projectId}/locations/${location}/connections/${connectionId}:fetchLinkableRepositories`; + const res = await client.get(name, { + queryParams: { + pageSize, + pageToken, + }, + }); + return res.body; +} + +/** + * Creates a Cloud Build V2 Repository. + */ +export async function createRepository( + projectId: string, + location: string, + connectionId: string, + repositoryId: string, + remoteUri: string, +): Promise { + const res = await client.post, Operation>( + `projects/${projectId}/locations/${location}/connections/${connectionId}/repositories`, + { remoteUri }, + { queryParams: { repositoryId } }, + ); + return res.body; +} + +/** + * Gets metadata for a Cloud Build V2 Repository. + */ +export async function getRepository( + projectId: string, + location: string, + connectionId: string, + repositoryId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/connections/${connectionId}/repositories/${repositoryId}`; + const res = await client.get(name); + return res.body; +} + +/** + * Deletes a Cloud Build V2 Repository. + */ +export async function deleteRepository( + projectId: string, + location: string, + connectionId: string, + repositoryId: string, +) { + const name = `projects/${projectId}/locations/${location}/connections/${connectionId}/repositories/${repositoryId}`; + const res = await client.delete(name); + return res.body; +} + +/** + * Returns the service account created by Cloud Build to use as a default in Cloud Build jobs. + * This service account is deprecated and future users should bring their own account. + */ +export function getDefaultServiceAccount(projectNumber: string): string { + return `${projectNumber}@cloudbuild.gserviceaccount.com`; +} + +/** + * Returns the default cloud build service agent. + * This is the account that Cloud Build itself uses when performing operations on the user's behalf. + */ +export function getDefaultServiceAgent(projectNumber: string): string { + return `service-${projectNumber}@gcp-sa-cloudbuild.iam.gserviceaccount.com`; +} diff --git a/src/gcp/cloudfunctions.spec.ts b/src/gcp/cloudfunctions.spec.ts new file mode 100644 index 00000000000..bd0b775bfd9 --- /dev/null +++ b/src/gcp/cloudfunctions.spec.ts @@ -0,0 +1,744 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import { functionsOrigin } from "../api"; + +import * as backend from "../deploy/functions/backend"; +import { BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT } from "../functions/events/v1"; +import * as cloudfunctions from "./cloudfunctions"; +import * as projectConfig from "../functions/projectConfig"; +import { BLOCKING_LABEL, CODEBASE_LABEL, HASH_LABEL } from "../functions/constants"; +import { FirebaseError } from "../error"; + +describe("cloudfunctions", () => { + const FUNCTION_NAME: backend.TargetIds = { + id: "id", + region: "region", + project: "project", + }; + + // Omit a random trigger to make this compile + const ENDPOINT: Omit = { + platform: "gcfv1", + ...FUNCTION_NAME, + entryPoint: "function", + runtime: "nodejs16", + codebase: projectConfig.DEFAULT_CODEBASE, + }; + + const CLOUD_FUNCTION: Omit = { + name: "projects/project/locations/region/functions/id", + entryPoint: "function", + runtime: "nodejs16", + dockerRegistry: "ARTIFACT_REGISTRY", + }; + + const HAVE_CLOUD_FUNCTION: cloudfunctions.CloudFunction = { + ...CLOUD_FUNCTION, + buildId: "buildId", + versionId: 1, + updateTime: new Date(), + status: "ACTIVE", + }; + + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + expect(nock.isDone()).to.be.true; + nock.enableNetConnect(); + }); + + describe("functionFromEndpoint", () => { + const UPLOAD_URL = "https://storage.googleapis.com/projects/-/buckets/sample/source.zip"; + it("should guard against version mixing", () => { + expect(() => { + cloudfunctions.functionFromEndpoint( + { ...ENDPOINT, platform: "gcfv2", httpsTrigger: {} }, + UPLOAD_URL, + ); + }).to.throw(); + }); + + it("should copy a minimal function", () => { + expect( + cloudfunctions.functionFromEndpoint({ ...ENDPOINT, httpsTrigger: {} }, UPLOAD_URL), + ).to.deep.equal({ + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + }); + + const eventEndpoint = { + ...ENDPOINT, + eventTrigger: { + eventType: "google.pubsub.topic.publish", + eventFilters: { resource: "projects/p/topics/t" }, + retry: false, + }, + }; + const eventGcfFunction = { + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: "projects/p/topics/t", + failurePolicy: undefined, + }, + }; + expect(cloudfunctions.functionFromEndpoint(eventEndpoint, UPLOAD_URL)).to.deep.equal( + eventGcfFunction, + ); + }); + + it("should copy trival fields", () => { + const fullEndpoint: backend.Endpoint = { + ...ENDPOINT, + httpsTrigger: {}, + availableMemoryMb: 128, + minInstances: 1, + maxInstances: 42, + vpc: { + connector: "connector", + egressSettings: "ALL_TRAFFIC", + }, + ingressSettings: "ALLOW_ALL", + serviceAccount: "inlined@google.com", + labels: { + foo: "bar", + }, + environmentVariables: { + FOO: "bar", + }, + }; + + const fullGcfFunction: Omit = { + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + labels: { + ...CLOUD_FUNCTION.labels, + foo: "bar", + }, + environmentVariables: { + FOO: "bar", + }, + maxInstances: 42, + minInstances: 1, + vpcConnector: "connector", + vpcConnectorEgressSettings: "ALL_TRAFFIC", + ingressSettings: "ALLOW_ALL", + availableMemoryMb: 128, + serviceAccountEmail: "inlined@google.com", + }; + + expect(cloudfunctions.functionFromEndpoint(fullEndpoint, UPLOAD_URL)).to.deep.equal( + fullGcfFunction, + ); + }); + + it("should calculate non-trivial fields", () => { + const complexEndpoint: backend.Endpoint = { + ...ENDPOINT, + scheduleTrigger: {}, + timeoutSeconds: 20, + }; + + const complexGcfFunction: Omit< + cloudfunctions.CloudFunction, + cloudfunctions.OutputOnlyFields + > = { + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: `projects/project/topics/${backend.scheduleIdForFunction(FUNCTION_NAME)}`, + }, + timeout: "20s", + labels: { + ...CLOUD_FUNCTION.labels, + "deployment-scheduled": "true", + }, + }; + + expect(cloudfunctions.functionFromEndpoint(complexEndpoint, UPLOAD_URL)).to.deep.equal( + complexGcfFunction, + ); + }); + + it("detects task queue functions", () => { + const taskEndpoint: backend.Endpoint = { + ...ENDPOINT, + taskQueueTrigger: {}, + }; + const taskQueueFunction: Omit = + { + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + labels: { + ...CLOUD_FUNCTION.labels, + "deployment-taskqueue": "true", + }, + }; + + expect(cloudfunctions.functionFromEndpoint(taskEndpoint, UPLOAD_URL)).to.deep.equal( + taskQueueFunction, + ); + }); + + it("detects beforeCreate blocking functions", () => { + const blockingEndpoint: backend.Endpoint = { + ...ENDPOINT, + blockingTrigger: { + eventType: BEFORE_CREATE_EVENT, + }, + }; + const blockingFunction: Omit = + { + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + labels: { + ...CLOUD_FUNCTION.labels, + [BLOCKING_LABEL]: "before-create", + }, + }; + + expect(cloudfunctions.functionFromEndpoint(blockingEndpoint, UPLOAD_URL)).to.deep.equal( + blockingFunction, + ); + }); + + it("detects beforeSignIn blocking functions", () => { + const blockingEndpoint: backend.Endpoint = { + ...ENDPOINT, + blockingTrigger: { + eventType: BEFORE_SIGN_IN_EVENT, + }, + }; + const blockingFunction: Omit = + { + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + labels: { + ...CLOUD_FUNCTION.labels, + [BLOCKING_LABEL]: "before-sign-in", + }, + }; + + expect(cloudfunctions.functionFromEndpoint(blockingEndpoint, UPLOAD_URL)).to.deep.equal( + blockingFunction, + ); + }); + + it("should export codebase as label", () => { + expect( + cloudfunctions.functionFromEndpoint( + { + ...ENDPOINT, + codebase: "my-codebase", + httpsTrigger: {}, + }, + UPLOAD_URL, + ), + ).to.deep.equal({ + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + labels: { ...CLOUD_FUNCTION.labels, [CODEBASE_LABEL]: "my-codebase" }, + }); + }); + + it("should export hash as label", () => { + expect( + cloudfunctions.functionFromEndpoint( + { + ...ENDPOINT, + hash: "my-hash", + httpsTrigger: {}, + }, + UPLOAD_URL, + ), + ).to.deep.equal({ + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + labels: { ...CLOUD_FUNCTION.labels, [HASH_LABEL]: "my-hash" }, + }); + }); + }); + + describe("endpointFromFunction", () => { + it("should copy a minimal version", () => { + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + httpsTrigger: {}, + }), + ).to.deep.equal({ ...ENDPOINT, httpsTrigger: {} }); + }); + + it("should translate event triggers", () => { + let want: backend.Endpoint = { + ...ENDPOINT, + eventTrigger: { + eventType: "google.pubsub.topic.publish", + eventFilters: { resource: "projects/p/topics/t" }, + retry: true, + }, + }; + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: "projects/p/topics/t", + failurePolicy: { + retry: {}, + }, + }, + }), + ).to.deep.equal(want); + + // And again w/o the failure policy + want = { + ...want, + eventTrigger: { + ...want.eventTrigger, + retry: false, + }, + }; + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: "projects/p/topics/t", + }, + }), + ).to.deep.equal(want); + }); + + it("should translate scheduled triggers", () => { + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + eventTrigger: { + eventType: "google.pubsub.topic.publish", + resource: "projects/p/topics/t", + failurePolicy: { + retry: {}, + }, + }, + labels: { + "deployment-scheduled": "true", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + scheduleTrigger: {}, + labels: { + "deployment-scheduled": "true", + }, + }); + }); + + it("should translate task queue triggers", () => { + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + httpsTrigger: {}, + labels: { + "deployment-taskqueue": "true", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + taskQueueTrigger: {}, + labels: { + "deployment-taskqueue": "true", + }, + }); + }); + + it("should translate beforeCreate blocking triggers", () => { + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + httpsTrigger: {}, + labels: { + "deployment-blocking": "before-create", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + blockingTrigger: { + eventType: BEFORE_CREATE_EVENT, + }, + labels: { + "deployment-blocking": "before-create", + }, + }); + }); + + it("should translate beforeSignIn blocking triggers", () => { + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + httpsTrigger: {}, + labels: { + "deployment-blocking": "before-sign-in", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + blockingTrigger: { + eventType: BEFORE_SIGN_IN_EVENT, + }, + labels: { + "deployment-blocking": "before-sign-in", + }, + }); + }); + + it("should copy optional fields", () => { + const wantExtraFields: Partial = { + availableMemoryMb: 128, + minInstances: 1, + maxInstances: 42, + ingressSettings: "ALLOW_ALL", + serviceAccount: "inlined@google.com", + timeoutSeconds: 15, + labels: { + foo: "bar", + }, + environmentVariables: { + FOO: "bar", + }, + }; + const haveExtraFields: Partial = { + availableMemoryMb: 128, + minInstances: 1, + maxInstances: 42, + ingressSettings: "ALLOW_ALL", + serviceAccountEmail: "inlined@google.com", + timeout: "15s", + labels: { + foo: "bar", + }, + environmentVariables: { + FOO: "bar", + }, + }; + const vpcConnector = "connector"; + const vpcConnectorEgressSettings = "ALL_TRAFFIC"; + + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + ...haveExtraFields, + vpcConnector, + vpcConnectorEgressSettings, + httpsTrigger: {}, + } as cloudfunctions.CloudFunction), + ).to.deep.equal({ + ...ENDPOINT, + ...wantExtraFields, + vpc: { + connector: vpcConnector, + egressSettings: vpcConnectorEgressSettings, + }, + httpsTrigger: {}, + }); + }); + + it("should derive codebase from labels", () => { + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + httpsTrigger: {}, + labels: { + ...CLOUD_FUNCTION.labels, + [CODEBASE_LABEL]: "my-codebase", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + httpsTrigger: {}, + labels: { + ...ENDPOINT.labels, + [CODEBASE_LABEL]: "my-codebase", + }, + codebase: "my-codebase", + }); + }); + + it("should derive hash from labels", () => { + expect( + cloudfunctions.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION, + httpsTrigger: {}, + labels: { + ...CLOUD_FUNCTION.labels, + [CODEBASE_LABEL]: "my-codebase", + [HASH_LABEL]: "my-hash", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + httpsTrigger: {}, + labels: { + ...ENDPOINT.labels, + [CODEBASE_LABEL]: "my-codebase", + [HASH_LABEL]: "my-hash", + }, + codebase: "my-codebase", + hash: "my-hash", + }); + }); + }); + + describe("setInvokerCreate", () => { + it("should reject on emtpy invoker array", async () => { + await expect(cloudfunctions.setInvokerCreate("project", "function", [])).to.be.rejected; + }); + + it("should reject if the setting the IAM policy fails", async () => { + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [{ role: "roles/cloudfunctions.invoker", members: ["allUsers"] }], + etag: "", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(418, {}); + + await expect( + cloudfunctions.setInvokerCreate("project", "function", ["public"]), + ).to.be.rejectedWith("Failed to set the IAM Policy on the function function"); + }); + + it("should set a private policy on a function", async () => { + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [{ role: "roles/cloudfunctions.invoker", members: [] }], + etag: "", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(200, {}); + + await expect(cloudfunctions.setInvokerCreate("project", "function", ["private"])).to.not.be + .rejected; + }); + + it("should set a public policy on a function", async () => { + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [{ role: "roles/cloudfunctions.invoker", members: ["allUsers"] }], + etag: "", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(200, {}); + + await expect(cloudfunctions.setInvokerCreate("project", "function", ["public"])).to.not.be + .rejected; + }); + + it("should set the policy with a set of invokers with active policies", async () => { + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [ + { + role: "roles/cloudfunctions.invoker", + members: [ + "serviceAccount:service-account1@project.iam.gserviceaccount.com", + "serviceAccount:service-account2@project.iam.gserviceaccount.com", + "serviceAccount:service-account3@project.iam.gserviceaccount.com", + ], + }, + ], + etag: "", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(200, {}); + + await expect( + cloudfunctions.setInvokerCreate("project", "function", [ + "service-account1@", + "service-account2@project.iam.gserviceaccount.com", + "service-account3@", + ]), + ).to.not.be.rejected; + }); + }); + + describe("setInvokerUpdate", () => { + it("should reject on emtpy invoker array", async () => { + await expect(cloudfunctions.setInvokerUpdate("project", "function", [])).to.be.rejected; + }); + + it("should reject if the getting the IAM policy fails", async () => { + nock(functionsOrigin()).get("/v1/function:getIamPolicy").reply(404, {}); + + await expect( + cloudfunctions.setInvokerUpdate("project", "function", ["public"]), + ).to.be.rejectedWith("Failed to get the IAM Policy on the function function"); + }); + + it("should reject if the setting the IAM policy fails", async () => { + nock(functionsOrigin()).get("/v1/function:getIamPolicy").reply(200, {}); + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [{ role: "roles/cloudfunctions.invoker", members: ["allUsers"] }], + etag: "", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(418, {}); + + await expect( + cloudfunctions.setInvokerUpdate("project", "function", ["public"]), + ).to.be.rejectedWith("Failed to set the IAM Policy on the function function"); + }); + + it("should set a basic policy on a function without any polices", async () => { + nock(functionsOrigin()).get("/v1/function:getIamPolicy").reply(200, {}); + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [{ role: "roles/cloudfunctions.invoker", members: ["allUsers"] }], + etag: "", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(200, {}); + + await expect(cloudfunctions.setInvokerUpdate("project", "function", ["public"])).to.not.be + .rejected; + }); + + it("should set the policy with private invoker with active policies", async () => { + nock(functionsOrigin()) + .get("/v1/function:getIamPolicy") + .reply(200, { + bindings: [ + { role: "random-role", members: ["user:pineapple"] }, + { role: "roles/cloudfunctions.invoker", members: ["some-service-account"] }, + ], + etag: "1234", + version: 3, + }); + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [ + { role: "random-role", members: ["user:pineapple"] }, + { role: "roles/cloudfunctions.invoker", members: [] }, + ], + etag: "1234", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(200, {}); + + await expect(cloudfunctions.setInvokerUpdate("project", "function", ["private"])).to.not.be + .rejected; + }); + + it("should set the policy with a set of invokers with active policies", async () => { + nock(functionsOrigin()).get("/v1/function:getIamPolicy").reply(200, {}); + nock(functionsOrigin()) + .post("/v1/function:setIamPolicy", { + policy: { + bindings: [ + { + role: "roles/cloudfunctions.invoker", + members: [ + "serviceAccount:service-account1@project.iam.gserviceaccount.com", + "serviceAccount:service-account2@project.iam.gserviceaccount.com", + "serviceAccount:service-account3@project.iam.gserviceaccount.com", + ], + }, + ], + etag: "", + version: 3, + }, + updateMask: "bindings,etag,version", + }) + .reply(200, {}); + + await expect( + cloudfunctions.setInvokerUpdate("project", "function", [ + "service-account1@", + "service-account2@project.iam.gserviceaccount.com", + "service-account3@", + ]), + ).to.not.be.rejected; + }); + + it("should not set the policy if the set of invokers is the same as the current invokers", async () => { + nock(functionsOrigin()) + .get("/v1/function:getIamPolicy") + .reply(200, { + bindings: [ + { + role: "roles/cloudfunctions.invoker", + members: [ + "serviceAccount:service-account1@project.iam.gserviceaccount.com", + "serviceAccount:service-account3@project.iam.gserviceaccount.com", + "serviceAccount:service-account2@project.iam.gserviceaccount.com", + ], + }, + ], + etag: "1234", + version: 3, + }); + + await expect( + cloudfunctions.setInvokerUpdate("project", "function", [ + "service-account2@project.iam.gserviceaccount.com", + "service-account3@", + "service-account1@", + ]), + ).to.not.be.rejected; + }); + }); + + describe("listFunctions", () => { + it("should pass back an error with the correct status", async () => { + nock(functionsOrigin()) + .get("/v1/projects/foo/locations/-/functions") + .reply(403, { error: "You don't have permissions." }); + + let errCaught = false; + try { + await cloudfunctions.listFunctions("foo", "-"); + } catch (err: unknown) { + errCaught = true; + expect(err).instanceOf(FirebaseError); + expect(err).has.property("status", 403); + } + + expect(errCaught, "should have caught an error").to.be.true; + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/gcp/cloudfunctions.ts b/src/gcp/cloudfunctions.ts index e1fe8ecb6c4..763135de952 100644 --- a/src/gcp/cloudfunctions.ts +++ b/src/gcp/cloudfunctions.ts @@ -1,16 +1,26 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { FirebaseError } from "../error"; import { logger } from "../logger"; -import { previews } from "../previews"; -import * as api from "../api"; import * as backend from "../deploy/functions/backend"; import * as utils from "../utils"; import * as proto from "./proto"; -import * as runtimes from "../deploy/functions/runtimes"; +import * as supported from "../deploy/functions/runtimes/supported"; import * as iam from "./iam"; +import * as projectConfig from "../functions/projectConfig"; +import { Client } from "../apiv2"; +import { functionsOrigin } from "../api"; +import { AUTH_BLOCKING_EVENTS } from "../functions/events/v1"; +import { + BLOCKING_EVENT_TO_LABEL_KEY, + BLOCKING_LABEL, + BLOCKING_LABEL_KEY_TO_EVENT, + CODEBASE_LABEL, + HASH_LABEL, +} from "../functions/constants"; export const API_VERSION = "v1"; +const client = new Client({ urlPrefix: functionsOrigin(), apiVersion: API_VERSION }); interface Operation { name: string; @@ -43,7 +53,7 @@ export interface SecretEnvVar { key: string; projectId: string; secret: string; - version: string; + version?: string; } export interface SecretVolume { @@ -70,6 +80,10 @@ export interface FailurePolicy { // end oneof action } +/** + * API type for Cloud Functions in the v1 API. Fields that are nullable can + * be set to null in UpdateFunction to reset them to default server-side values. + */ export interface CloudFunction { name: string; description?: string; @@ -89,33 +103,34 @@ export interface CloudFunction { // end oneof trigger; entryPoint: string; - runtime: runtimes.Runtime; - // Seconds. Default = 60 - timeout?: proto.Duration; + runtime: supported.Runtime; + // Default = 60s + timeout?: proto.Duration | null; // Default 256 - availableMemoryMb?: number; + availableMemoryMb?: number | null; // Default @appspot.gserviceaccount.com - serviceAccountEmail?: string; + serviceAccountEmail?: string | null; labels?: Record; - environmentVariables?: Record; + environmentVariables?: Record | null; buildEnvironmentVariables?: Record; - network?: string; - maxInstances?: number; - minInstances?: number; + network?: string | null; + maxInstances?: number | null; + minInstances?: number | null; corsPolicy?: CorsPolicy; - vpcConnector?: string; - vpcConnectorEgressSettings?: "PRIVATE_RANGES_ONLY" | "ALL_TRAFFIC"; - ingressSettings?: "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB"; + vpcConnector?: string | null; + vpcConnectorEgressSettings?: "PRIVATE_RANGES_ONLY" | "ALL_TRAFFIC" | null; + ingressSettings?: "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB" | null; - kmsKeyName?: string; - buildWorkerPool?: string; - secretEnvironmentVariables?: SecretEnvVar[]; - secretVolumes?: SecretVolume[]; + kmsKeyName?: string | null; + buildWorkerPool?: string | null; + secretEnvironmentVariables?: SecretEnvVar[] | null; + secretVolumes?: SecretVolume[] | null; + dockerRegistry?: "CONTAINER_REGISTRY" | "ARTIFACT_REGISTRY"; // Input-only parameter. Source token originally comes from the Operation // of another Create/Update function call. @@ -130,18 +145,6 @@ export interface CloudFunction { export type OutputOnlyFields = "status" | "buildId" | "updateTime" | "versionId"; -function validateFunction(func: CloudFunction) { - proto.assertOneOf( - "Cloud Function", - func, - "sourceCode", - "sourceArchiveUrl", - "sourceRepository", - "sourceUploadUrl" - ); - proto.assertOneOf("Cloud Function", func, "trigger", "httpsTrigger", "eventTrigger"); -} - /** * Logs an error from a failed function deployment. * @param funcName Name of the function that was unsuccessfully deployed. @@ -151,17 +154,18 @@ function validateFunction(func: CloudFunction) { function functionsOpLogReject(funcName: string, type: string, err: any): void { if (err?.context?.response?.statusCode === 429) { utils.logWarning( - `${clc.bold.yellow( - "functions:" - )} got "Quota Exceeded" error while trying to ${type} ${funcName}. Waiting to retry...` + `${clc.bold( + clc.yellow("functions:"), + )} got "Quota Exceeded" error while trying to ${type} ${funcName}. Waiting to retry...`, ); } else { utils.logWarning( - clc.bold.yellow("functions:") + " failed to " + type + " function " + funcName + clc.bold(clc.yellow("functions:")) + " failed to " + type + " function " + funcName, ); } throw new FirebaseError(`Failed to ${type} function ${funcName}`, { original: err, + status: err?.context?.response?.statusCode, context: { function: funcName }, }); } @@ -174,20 +178,18 @@ function functionsOpLogReject(funcName: string, type: string, err: any): void { */ export async function generateUploadUrl(projectId: string, location: string): Promise { const parent = "projects/" + projectId + "/locations/" + location; - const endpoint = "/" + API_VERSION + "/" + parent + "/functions:generateUploadUrl"; + const endpoint = `/${parent}/functions:generateUploadUrl`; try { - const res = await api.request("POST", endpoint, { - auth: true, - json: false, - origin: api.functionsOrigin, - retryCodes: [503], - }); - const responseBody = JSON.parse(res.body); - return responseBody.uploadUrl; - } catch (err) { + const res = await client.post( + endpoint, + {}, + { retryCodes: [503] }, + ); + return res.body.uploadUrl; + } catch (err: any) { logger.info( - "\n\nThere was an issue deploying your functions. Verify that your project has a Google App Engine instance setup at https://console.cloud.google.com/appengine and try again. If this issue persists, please contact support." + "\n\nThere was an issue deploying your functions. Verify that your project has a Google App Engine instance setup at https://console.cloud.google.com/appengine and try again. If this issue persists, please contact support.", ); throw err; } @@ -198,29 +200,29 @@ export async function generateUploadUrl(projectId: string, location: string): Pr * @param cloudFunction The function to delete */ export async function createFunction( - cloudFunction: Omit + cloudFunction: Omit, ): Promise { // the API is a POST to the collection that owns the function name. const apiPath = cloudFunction.name.substring(0, cloudFunction.name.lastIndexOf("/")); - const endpoint = `/${API_VERSION}/${apiPath}`; + const endpoint = `/${apiPath}`; + cloudFunction.buildEnvironmentVariables = { + ...cloudFunction.buildEnvironmentVariables, + // Disable GCF from automatically running npm run build script + // https://cloud.google.com/functions/docs/release-notes + GOOGLE_NODE_RUN_SCRIPTS: "", + }; try { - const headers: Record = {}; - if (previews.artifactregistry) { - headers["X-Firebase-Artifact-Registry"] = "optin"; - } - const res = await api.request("POST", endpoint, { - headers, - auth: true, - data: cloudFunction, - origin: api.functionsOrigin, - }); + const res = await client.post, CloudFunction>( + endpoint, + cloudFunction, + ); return { name: res.body.name, type: "create", done: false, }; - } catch (err) { + } catch (err: any) { throw functionsOpLogReject(cloudFunction.name, "create", err); } } @@ -239,20 +241,17 @@ interface IamOptions { * @param options The Iam options to set. */ export async function setIamPolicy(options: IamOptions): Promise { - const endpoint = `/${API_VERSION}/${options.name}:setIamPolicy`; + const endpoint = `/${options.name}:setIamPolicy`; try { - await api.request("POST", endpoint, { - auth: true, - data: { - policy: options.policy, - updateMask: Object.keys(options.policy).join(","), - }, - origin: api.functionsOrigin, + await client.post(endpoint, { + policy: options.policy, + updateMask: Object.keys(options.policy).join(","), }); - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to set the IAM Policy on the function ${options.name}`, { original: err, + status: err?.context?.response?.statusCode, }); } } @@ -269,14 +268,12 @@ interface GetIamPolicy { * @param fnName The full name and path of the Cloud Function. */ export async function getIamPolicy(fnName: string): Promise { - const endpoint = `/${API_VERSION}/${fnName}:getIamPolicy`; + const endpoint = `/${fnName}:getIamPolicy`; try { - return await api.request("GET", endpoint, { - auth: true, - origin: api.functionsOrigin, - }); - } catch (err) { + const res = await client.get(endpoint); + return res.body; + } catch (err: any) { throw new FirebaseError(`Failed to get the IAM Policy on the function ${fnName}`, { original: err, }); @@ -288,15 +285,14 @@ export async function getIamPolicy(fnName: string): Promise { * @param projectId id of the project * @param fnName function name * @param invoker an array of invoker strings - * * @throws {@link FirebaseError} on an empty invoker, when the IAM Polciy fails to be grabbed or set */ export async function setInvokerCreate( projectId: string, fnName: string, - invoker: string[] + invoker: string[], ): Promise { - if (invoker.length == 0) { + if (invoker.length === 0) { throw new FirebaseError("Invoker cannot be an empty array"); } const invokerMembers = proto.getInvokerMembers(invoker, projectId); @@ -317,22 +313,21 @@ export async function setInvokerCreate( * @param projectId id of the project * @param fnName function name * @param invoker an array of invoker strings - * * @throws {@link FirebaseError} on an empty invoker, when the IAM Polciy fails to be grabbed or set */ export async function setInvokerUpdate( projectId: string, fnName: string, - invoker: string[] + invoker: string[], ): Promise { - if (invoker.length == 0) { + if (invoker.length === 0) { throw new FirebaseError("Invoker cannot be an empty array"); } const invokerMembers = proto.getInvokerMembers(invoker, projectId); const invokerRole = "roles/cloudfunctions.invoker"; const currentPolicy = await getIamPolicy(fnName); const currentInvokerBinding = currentPolicy.bindings?.find( - (binding) => binding.role === invokerRole + (binding) => binding.role === invokerRole, ); if ( currentInvokerBinding && @@ -360,39 +355,44 @@ export async function setInvokerUpdate( * @param cloudFunction The Cloud Function to update. */ export async function updateFunction( - cloudFunction: Omit + cloudFunction: Omit, ): Promise { - const endpoint = `/${API_VERSION}/${cloudFunction.name}`; - // Keys in labels and environmentVariables are user defined, so we don't recurse - // for field masks. + const endpoint = `/${cloudFunction.name}`; + // Keys in labels and environmentVariables and secretEnvironmentVariables are user defined, + // so we don't recurse for field masks. const fieldMasks = proto.fieldMasks( cloudFunction, /* doNotRecurseIn...=*/ "labels", - "environmentVariables" + "environmentVariables", + "secretEnvironmentVariables", ); + cloudFunction.buildEnvironmentVariables = { + ...cloudFunction.buildEnvironmentVariables, + // Disable GCF from automatically running npm run build script + // https://cloud.google.com/functions/docs/release-notes + GOOGLE_NODE_RUN_SCRIPTS: "", + }; + fieldMasks.push("buildEnvironmentVariables"); + // Failure policy is always an explicit policy and is only signified by the presence or absence of // a protobuf.Empty value, so we have to manually add it in the missing case. try { - const headers: Record = {}; - if (previews.artifactregistry) { - headers["X-Firebase-Artifact-Registry"] = "optin"; - } - const res = await api.request("PATCH", endpoint, { - headers, - qs: { - updateMask: fieldMasks.join(","), + const res = await client.patch, CloudFunction>( + endpoint, + cloudFunction, + { + queryParams: { + updateMask: fieldMasks.join(","), + }, }, - auth: true, - data: cloudFunction, - origin: api.functionsOrigin, - }); + ); return { done: false, name: res.body.name, type: "update", }; - } catch (err) { + } catch (err: any) { throw functionsOpLogReject(cloudFunction.name, "update", err); } } @@ -402,18 +402,15 @@ export async function updateFunction( * @param options the Cloud Function to delete. */ export async function deleteFunction(name: string): Promise { - const endpoint = `/${API_VERSION}/${name}`; + const endpoint = `/${name}`; try { - const res = await api.request("DELETE", endpoint, { - auth: true, - origin: api.functionsOrigin, - }); + const res = await client.delete(endpoint); return { done: false, name: res.body.name, type: "delete", }; - } catch (err) { + } catch (err: any) { throw functionsOpLogReject(name, "delete", err); } } @@ -424,16 +421,12 @@ export type ListFunctionsResponse = { }; async function list(projectId: string, region: string): Promise { - const endpoint = - "/" + API_VERSION + "/projects/" + projectId + "/locations/" + region + "/functions"; + const endpoint = "/projects/" + projectId + "/locations/" + region + "/functions"; try { - const res = await api.request("GET", endpoint, { - auth: true, - origin: api.functionsOrigin, - }); + const res = await client.get(endpoint); if (res.body.unreachable && res.body.unreachable.length > 0) { logger.debug( - `[functions] unable to reach the following regions: ${res.body.unreachable.join(", ")}` + `[functions] unable to reach the following regions: ${res.body.unreachable.join(", ")}`, ); } @@ -441,11 +434,12 @@ async function list(projectId: string, region: string): Promise raw as backend.MemoryOptions, + ); + proto.convertIfPresent(endpoint, gcfFunction, "timeoutSeconds", "timeout", (dur) => + dur === null ? null : proto.secondsFromDuration(dur), + ); + if (gcfFunction.vpcConnector) { + endpoint.vpc = { connector: gcfFunction.vpcConnector }; + proto.convertIfPresent( + endpoint.vpc, + gcfFunction, + "egressSettings", + "vpcConnectorEgressSettings", + (raw) => raw as backend.VpcEgressSettings, + ); + } + endpoint.codebase = gcfFunction.labels?.[CODEBASE_LABEL] || projectConfig.DEFAULT_CODEBASE; + if (gcfFunction.labels?.[HASH_LABEL]) { + endpoint.hash = gcfFunction.labels[HASH_LABEL]; + } return endpoint; } @@ -541,18 +584,19 @@ export function endpointFromFunction(gcfFunction: CloudFunction): backend.Endpoi */ export function functionFromEndpoint( endpoint: backend.Endpoint, - sourceUploadUrl: string + sourceUploadUrl: string, ): Omit { - if (endpoint.platform != "gcfv1") { + if (endpoint.platform !== "gcfv1") { throw new FirebaseError( - "Trying to create a v1 CloudFunction with v2 API. This should never happen" + "Trying to create a v1 CloudFunction with v2 API. This should never happen", ); } - if (!runtimes.isValidRuntime(endpoint.runtime)) { + if (!supported.isRuntime(endpoint.runtime)) { throw new FirebaseError( "Failed internal assertion. Trying to deploy a new function with a deprecated runtime." + - " This should never happen" + " This should never happen", + { exit: 1 }, ); } const gcfFunction: Omit = { @@ -560,10 +604,19 @@ export function functionFromEndpoint( sourceUploadUrl: sourceUploadUrl, entryPoint: endpoint.entryPoint, runtime: endpoint.runtime, + dockerRegistry: "ARTIFACT_REGISTRY", }; - proto.copyIfPresent(gcfFunction, endpoint, "labels"); + // N.B. It has the same effect to set labels to the empty object as it does to + // set it to null, except the former is more effective for adding automatic + // lables for things like deployment-callable + if (typeof endpoint.labels !== "undefined") { + gcfFunction.labels = { ...endpoint.labels }; + } if (backend.isEventTriggered(endpoint)) { + if (!endpoint.eventTrigger.eventFilters?.resource) { + throw new FirebaseError("Cannot create v1 function from an eventTrigger without a resource"); + } gcfFunction.eventTrigger = { eventType: endpoint.eventTrigger.eventType, resource: endpoint.eventTrigger.eventFilters.resource, @@ -585,23 +638,70 @@ export function functionFromEndpoint( } else if (backend.isTaskQueueTriggered(endpoint)) { gcfFunction.httpsTrigger = {}; gcfFunction.labels = { ...gcfFunction.labels, "deployment-taskqueue": "true" }; + } else if (backend.isBlockingTriggered(endpoint)) { + gcfFunction.httpsTrigger = {}; + gcfFunction.labels = { + ...gcfFunction.labels, + [BLOCKING_LABEL]: + BLOCKING_EVENT_TO_LABEL_KEY[ + endpoint.blockingTrigger.eventType as (typeof AUTH_BLOCKING_EVENTS)[number] + ], + }; } else { gcfFunction.httpsTrigger = {}; + if (backend.isCallableTriggered(endpoint)) { + gcfFunction.labels = { ...gcfFunction.labels, "deployment-callable": "true" }; + } + if (endpoint.securityLevel) { + gcfFunction.httpsTrigger.securityLevel = endpoint.securityLevel; + } } proto.copyIfPresent( gcfFunction, endpoint, - "serviceAccountEmail", - "timeout", - "availableMemoryMb", "minInstances", "maxInstances", - "vpcConnector", - "vpcConnectorEgressSettings", "ingressSettings", - "environmentVariables" + "environmentVariables", + "secretEnvironmentVariables", ); - + proto.renameIfPresent(gcfFunction, endpoint, "serviceAccountEmail", "serviceAccount"); + proto.convertIfPresent( + gcfFunction, + endpoint, + "availableMemoryMb", + (mem) => mem as backend.MemoryOptions, + ); + proto.convertIfPresent(gcfFunction, endpoint, "timeout", "timeoutSeconds", (sec) => + sec ? proto.durationFromSeconds(sec) : null, + ); + if (endpoint.vpc) { + proto.renameIfPresent(gcfFunction, endpoint.vpc, "vpcConnector", "connector"); + proto.renameIfPresent( + gcfFunction, + endpoint.vpc, + "vpcConnectorEgressSettings", + "egressSettings", + ); + } else if (endpoint.vpc === null) { + gcfFunction.vpcConnector = null; + gcfFunction.vpcConnectorEgressSettings = null; + } + const codebase = endpoint.codebase || projectConfig.DEFAULT_CODEBASE; + if (codebase !== projectConfig.DEFAULT_CODEBASE) { + gcfFunction.labels = { + ...gcfFunction.labels, + [CODEBASE_LABEL]: codebase, + }; + } else { + delete gcfFunction.labels?.[CODEBASE_LABEL]; + } + if (endpoint.hash) { + gcfFunction.labels = { + ...gcfFunction.labels, + [HASH_LABEL]: endpoint.hash, + }; + } return gcfFunction; } diff --git a/src/gcp/cloudfunctionsv2.spec.ts b/src/gcp/cloudfunctionsv2.spec.ts new file mode 100644 index 00000000000..ffe68467fff --- /dev/null +++ b/src/gcp/cloudfunctionsv2.spec.ts @@ -0,0 +1,795 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import * as cloudfunctionsv2 from "./cloudfunctionsv2"; +import * as backend from "../deploy/functions/backend"; +import * as events from "../functions/events"; +import * as projectConfig from "../functions/projectConfig"; +import { BLOCKING_LABEL, CODEBASE_LABEL, HASH_LABEL } from "../functions/constants"; +import { functionsV2Origin } from "../api"; +import { FirebaseError } from "../error"; + +describe("cloudfunctionsv2", () => { + const FUNCTION_NAME: backend.TargetIds = { + id: "id", + region: "region", + project: "project", + }; + + const CLOUD_FUNCTION_V2_SOURCE: cloudfunctionsv2.StorageSource = { + bucket: "sample", + object: "source.zip", + generation: 42, + }; + + // Omit a random trigger to get this fragment to compile. + const ENDPOINT: Omit = { + platform: "gcfv2", + ...FUNCTION_NAME, + entryPoint: "function", + runtime: "nodejs16", + codebase: projectConfig.DEFAULT_CODEBASE, + runServiceId: "service", + source: { storageSource: CLOUD_FUNCTION_V2_SOURCE }, + }; + + const CLOUD_FUNCTION_V2: cloudfunctionsv2.InputCloudFunction = { + name: "projects/project/locations/region/functions/id", + buildConfig: { + entryPoint: "function", + runtime: "nodejs16", + source: { + storageSource: CLOUD_FUNCTION_V2_SOURCE, + }, + environmentVariables: {}, + }, + serviceConfig: { + availableMemory: `${backend.DEFAULT_MEMORY}Mi`, + }, + }; + + const RUN_URI = "https://id-nonce-region-project.run.app"; + const HAVE_CLOUD_FUNCTION_V2: cloudfunctionsv2.OutputCloudFunction = { + ...CLOUD_FUNCTION_V2, + serviceConfig: { + service: "service", + uri: RUN_URI, + }, + state: "ACTIVE", + updateTime: new Date(), + }; + + describe("megabytes", () => { + enum Bytes { + KB = 1e3, + MB = 1e6, + GB = 1e9, + KiB = 1 << 10, + MiB = 1 << 20, + GiB = 1 << 30, + } + it("Should handle decimal SI units", () => { + expect(cloudfunctionsv2.mebibytes("1000k")).to.equal((1000 * Bytes.KB) / Bytes.MiB); + expect(cloudfunctionsv2.mebibytes("1.5M")).to.equal((1.5 * Bytes.MB) / Bytes.MiB); + expect(cloudfunctionsv2.mebibytes("1G")).to.equal(Bytes.GB / Bytes.MiB); + }); + it("Should handle binary SI units", () => { + expect(cloudfunctionsv2.mebibytes("1Mi")).to.equal(Bytes.MiB / Bytes.MiB); + expect(cloudfunctionsv2.mebibytes("1Gi")).to.equal(Bytes.GiB / Bytes.MiB); + }); + it("Should handle no unit", () => { + expect(cloudfunctionsv2.mebibytes("100000")).to.equal(100000 / Bytes.MiB); + expect(cloudfunctionsv2.mebibytes("1e9")).to.equal(1e9 / Bytes.MiB); + expect(cloudfunctionsv2.mebibytes("1.5E6")).to.equal((1.5 * 1e6) / Bytes.MiB); + }); + }); + describe("functionFromEndpoint", () => { + it("should guard against version mixing", () => { + expect(() => { + cloudfunctionsv2.functionFromEndpoint({ ...ENDPOINT, httpsTrigger: {}, platform: "gcfv1" }); + }).to.throw(); + }); + + it("should copy a minimal function", () => { + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + platform: "gcfv2", + httpsTrigger: {}, + }), + ).to.deep.equal(CLOUD_FUNCTION_V2); + + const eventEndpoint: backend.Endpoint = { + ...ENDPOINT, + platform: "gcfv2", + eventTrigger: { + eventType: "google.cloud.audit.log.v1.written", + eventFilters: { + resource: "projects/p/regions/r/instances/i", + serviceName: "compute.googleapis.com", + }, + retry: true, + channel: "projects/myproject/locations/us-wildwest11/channels/mychannel", + }, + }; + const eventGcfFunction: cloudfunctionsv2.InputCloudFunction = { + ...CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: "google.cloud.audit.log.v1.written", + eventFilters: [ + { + attribute: "resource", + value: "projects/p/regions/r/instances/i", + }, + { + attribute: "serviceName", + value: "compute.googleapis.com", + }, + ], + retryPolicy: "RETRY_POLICY_RETRY", + channel: "projects/myproject/locations/us-wildwest11/channels/mychannel", + }, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + environmentVariables: { FUNCTION_SIGNATURE_TYPE: "cloudevent" }, + }, + }; + expect(cloudfunctionsv2.functionFromEndpoint(eventEndpoint)).to.deep.equal(eventGcfFunction); + + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + platform: "gcfv2", + eventTrigger: { + eventType: "google.firebase.database.ref.v1.written", + eventFilters: { + instance: "my-db-1", + }, + eventFilterPathPatterns: { + path: "foo/{bar}", + }, + retry: false, + }, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: "google.firebase.database.ref.v1.written", + eventFilters: [ + { + attribute: "instance", + value: "my-db-1", + }, + { + attribute: "path", + value: "foo/{bar}", + operator: "match-path-pattern", + }, + ], + retryPolicy: "RETRY_POLICY_DO_NOT_RETRY", + }, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + environmentVariables: { FUNCTION_SIGNATURE_TYPE: "cloudevent" }, + }, + }); + + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + platform: "gcfv2", + taskQueueTrigger: {}, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + "deployment-taskqueue": "true", + }, + }); + + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + platform: "gcfv2", + blockingTrigger: { + eventType: events.v1.BEFORE_CREATE_EVENT, + }, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + [BLOCKING_LABEL]: "before-create", + }, + }); + + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + platform: "gcfv2", + blockingTrigger: { + eventType: events.v1.BEFORE_SIGN_IN_EVENT, + }, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + [BLOCKING_LABEL]: "before-sign-in", + }, + }); + }); + + it("should copy trival fields", () => { + const fullEndpoint: backend.Endpoint = { + ...ENDPOINT, + httpsTrigger: {}, + platform: "gcfv2", + vpc: { + connector: "connector", + egressSettings: "ALL_TRAFFIC", + }, + ingressSettings: "ALLOW_ALL", + serviceAccount: "inlined@google.com", + labels: { + foo: "bar", + }, + environmentVariables: { + FOO: "bar", + }, + secretEnvironmentVariables: [ + { + secret: "MY_SECRET", + key: "MY_SECRET", + projectId: "project", + }, + ], + }; + + const fullGcfFunction: cloudfunctionsv2.InputCloudFunction = { + ...CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + foo: "bar", + }, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + environmentVariables: { + FOO: "bar", + }, + secretEnvironmentVariables: [ + { + secret: "MY_SECRET", + key: "MY_SECRET", + projectId: "project", + }, + ], + vpcConnector: "connector", + vpcConnectorEgressSettings: "ALL_TRAFFIC", + ingressSettings: "ALLOW_ALL", + serviceAccountEmail: "inlined@google.com", + }, + }; + + expect(cloudfunctionsv2.functionFromEndpoint(fullEndpoint)).to.deep.equal(fullGcfFunction); + }); + + it("should calculate non-trivial fields", () => { + const complexEndpoint: backend.Endpoint = { + ...ENDPOINT, + platform: "gcfv2", + eventTrigger: { + eventType: events.v2.PUBSUB_PUBLISH_EVENT, + eventFilters: { + topic: "projects/p/topics/t", + serviceName: "pubsub.googleapis.com", + }, + retry: false, + }, + maxInstances: 42, + minInstances: 1, + timeoutSeconds: 15, + availableMemoryMb: 128, + }; + + const complexGcfFunction: cloudfunctionsv2.InputCloudFunction = { + ...CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: events.v2.PUBSUB_PUBLISH_EVENT, + pubsubTopic: "projects/p/topics/t", + eventFilters: [ + { + attribute: "serviceName", + value: "pubsub.googleapis.com", + }, + ], + retryPolicy: "RETRY_POLICY_DO_NOT_RETRY", + }, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + maxInstanceCount: 42, + minInstanceCount: 1, + timeoutSeconds: 15, + availableMemory: "128Mi", + environmentVariables: { FUNCTION_SIGNATURE_TYPE: "cloudevent" }, + }, + }; + + expect(cloudfunctionsv2.functionFromEndpoint(complexEndpoint)).to.deep.equal( + complexGcfFunction, + ); + }); + + it("should propagate serviceAccount to eventarc", () => { + const saEndpoint: backend.Endpoint = { + ...ENDPOINT, + platform: "gcfv2", + eventTrigger: { + eventType: events.v2.DATABASE_EVENTS[0], + eventFilters: { + ref: "ref", + }, + retry: false, + }, + serviceAccount: "sa", + }; + + const saGcfFunction: cloudfunctionsv2.InputCloudFunction = { + ...CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: events.v2.DATABASE_EVENTS[0], + eventFilters: [ + { + attribute: "ref", + value: "ref", + }, + ], + retryPolicy: "RETRY_POLICY_DO_NOT_RETRY", + serviceAccountEmail: "sa", + }, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + environmentVariables: { + FUNCTION_SIGNATURE_TYPE: "cloudevent", + }, + serviceAccountEmail: "sa", + }, + }; + + expect(cloudfunctionsv2.functionFromEndpoint(saEndpoint)).to.deep.equal(saGcfFunction); + }); + + it("should correctly convert CPU and concurrency values", () => { + const endpoint: backend.Endpoint = { + ...ENDPOINT, + platform: "gcfv2", + httpsTrigger: {}, + concurrency: 40, + cpu: 2, + }; + const gcfFunction: cloudfunctionsv2.InputCloudFunction = { + ...CLOUD_FUNCTION_V2, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + maxInstanceRequestConcurrency: 40, + availableCpu: "2", + }, + }; + expect(cloudfunctionsv2.functionFromEndpoint(endpoint)).to.deep.equal(gcfFunction); + }); + + it("should export codebase as label", () => { + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + codebase: "my-codebase", + httpsTrigger: {}, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + labels: { ...CLOUD_FUNCTION_V2.labels, [CODEBASE_LABEL]: "my-codebase" }, + }); + }); + + it("should export hash as label", () => { + expect( + cloudfunctionsv2.functionFromEndpoint({ ...ENDPOINT, hash: "my-hash", httpsTrigger: {} }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + labels: { ...CLOUD_FUNCTION_V2.labels, [HASH_LABEL]: "my-hash" }, + }); + }); + }); + + describe("endpointFromFunction", () => { + it("should copy a minimal version", () => { + expect(cloudfunctionsv2.endpointFromFunction(HAVE_CLOUD_FUNCTION_V2)).to.deep.equal({ + ...ENDPOINT, + httpsTrigger: {}, + platform: "gcfv2", + uri: RUN_URI, + }); + }); + + it("should copy run service IDs", () => { + const fn: cloudfunctionsv2.OutputCloudFunction = { + ...HAVE_CLOUD_FUNCTION_V2, + serviceConfig: { + ...HAVE_CLOUD_FUNCTION_V2.serviceConfig, + service: "projects/p/locations/l/services/service-id", + uri: RUN_URI, + }, + }; + expect(cloudfunctionsv2.endpointFromFunction(fn)).to.deep.equal({ + ...ENDPOINT, + httpsTrigger: {}, + platform: "gcfv2", + uri: RUN_URI, + runServiceId: "service-id", + }); + }); + + it("should translate event triggers", () => { + let want: backend.Endpoint = { + ...ENDPOINT, + platform: "gcfv2", + uri: RUN_URI, + eventTrigger: { + eventType: events.v2.PUBSUB_PUBLISH_EVENT, + eventFilters: { topic: "projects/p/topics/t" }, + retry: false, + }, + }; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: events.v2.PUBSUB_PUBLISH_EVENT, + pubsubTopic: "projects/p/topics/t", + }, + }), + ).to.deep.equal(want); + + // And again w/ a normal event trigger + want = { + ...want, + eventTrigger: { + eventType: "google.cloud.audit.log.v1.written", + eventFilters: { + resource: "projects/p/regions/r/instances/i", + serviceName: "compute.googleapis.com", + }, + retry: false, + }, + }; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: "google.cloud.audit.log.v1.written", + eventFilters: [ + { + attribute: "resource", + value: "projects/p/regions/r/instances/i", + }, + { + attribute: "serviceName", + value: "compute.googleapis.com", + }, + ], + }, + }), + ).to.deep.equal(want); + + // And again with a pattern match event trigger + want = { + ...want, + eventTrigger: { + eventType: "google.firebase.database.ref.v1.written", + eventFilters: { + instance: "my-db-1", + }, + eventFilterPathPatterns: { + path: "foo/{bar}", + }, + retry: false, + }, + }; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: "google.firebase.database.ref.v1.written", + eventFilters: [ + { + attribute: "instance", + value: "my-db-1", + }, + { + attribute: "path", + value: "foo/{bar}", + operator: "match-path-pattern", + }, + ], + }, + }), + ).to.deep.equal(want); + + // And again with a pattern match event trigger + want = { + ...want, + eventTrigger: { + eventType: "google.cloud.firestore.document.v1.written", + eventFilters: { + database: "(default)", + namespace: "(default)", + }, + eventFilterPathPatterns: { + document: "users/{userId}", + }, + retry: false, + }, + }; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: "google.cloud.firestore.document.v1.written", + eventFilters: [ + { + attribute: "database", + value: "(default)", + }, + { + attribute: "namespace", + value: "(default)", + }, + { + attribute: "document", + value: "users/{userId}", + operator: "match-path-pattern", + }, + ], + pubsubTopic: "eventarc-us-central1-abc", // firestore triggers use pubsub as transport + }, + }), + ).to.deep.equal(want); + }); + + it("should translate custom event triggers", () => { + const want: backend.Endpoint = { + ...ENDPOINT, + platform: "gcfv2", + uri: RUN_URI, + eventTrigger: { + eventType: "com.custom.event", + eventFilters: { customattr: "customvalue" }, + channel: "projects/myproject/locations/us-wildwest11/channels/mychannel", + retry: false, + }, + }; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + eventTrigger: { + eventType: "com.custom.event", + eventFilters: [ + { + attribute: "customattr", + value: "customvalue", + }, + ], + channel: "projects/myproject/locations/us-wildwest11/channels/mychannel", + }, + }), + ).to.deep.equal(want); + }); + + it("should translate task queue functions", () => { + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + labels: { "deployment-taskqueue": "true" }, + }), + ).to.deep.equal({ + ...ENDPOINT, + taskQueueTrigger: {}, + platform: "gcfv2", + uri: RUN_URI, + labels: { "deployment-taskqueue": "true" }, + }); + }); + + it("should translate beforeCreate blocking functions", () => { + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + labels: { "deployment-blocking": "before-create" }, + }), + ).to.deep.equal({ + ...ENDPOINT, + blockingTrigger: { + eventType: events.v1.BEFORE_CREATE_EVENT, + }, + platform: "gcfv2", + uri: RUN_URI, + labels: { "deployment-blocking": "before-create" }, + }); + }); + + it("should translate beforeSignIn blocking functions", () => { + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + labels: { "deployment-blocking": "before-sign-in" }, + }), + ).to.deep.equal({ + ...ENDPOINT, + blockingTrigger: { + eventType: events.v1.BEFORE_SIGN_IN_EVENT, + }, + platform: "gcfv2", + uri: RUN_URI, + labels: { "deployment-blocking": "before-sign-in" }, + }); + }); + + it("should copy optional fields", () => { + const extraFields: backend.ServiceConfiguration = { + ingressSettings: "ALLOW_ALL", + timeoutSeconds: 15, + environmentVariables: { + FOO: "bar", + }, + }; + const vpc = { + connector: "connector", + egressSettings: "ALL_TRAFFIC" as const, + }; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + serviceConfig: { + ...HAVE_CLOUD_FUNCTION_V2.serviceConfig, + ...extraFields, + serviceAccountEmail: "inlined@google.com", + vpcConnector: vpc.connector, + vpcConnectorEgressSettings: vpc.egressSettings, + availableMemory: "128Mi", + uri: RUN_URI, + service: "service", + }, + labels: { + foo: "bar", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + platform: "gcfv2", + httpsTrigger: {}, + uri: RUN_URI, + ...extraFields, + serviceAccount: "inlined@google.com", + vpc, + availableMemoryMb: 128, + labels: { + foo: "bar", + }, + }); + }); + + it("should transform fields", () => { + const extraFields: backend.ServiceConfiguration = { + minInstances: 1, + maxInstances: 42, + }; + + const extraGcfFields: Partial = { + minInstanceCount: 1, + maxInstanceCount: 42, + }; + + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + serviceConfig: { + ...HAVE_CLOUD_FUNCTION_V2.serviceConfig, + ...extraGcfFields, + uri: RUN_URI, + service: "service", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + platform: "gcfv2", + uri: RUN_URI, + httpsTrigger: {}, + ...extraFields, + }); + }); + + it("should derive codebase from labels", () => { + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + [CODEBASE_LABEL]: "my-codebase", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + platform: "gcfv2", + uri: RUN_URI, + httpsTrigger: {}, + labels: { + ...ENDPOINT.labels, + [CODEBASE_LABEL]: "my-codebase", + }, + codebase: "my-codebase", + }); + }); + + it("should derive hash from labels", () => { + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + [CODEBASE_LABEL]: "my-codebase", + [HASH_LABEL]: "my-hash", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + platform: "gcfv2", + uri: RUN_URI, + httpsTrigger: {}, + labels: { + ...ENDPOINT.labels, + [CODEBASE_LABEL]: "my-codebase", + [HASH_LABEL]: "my-hash", + }, + codebase: "my-codebase", + hash: "my-hash", + }); + }); + + it("should convert function without serviceConfig", () => { + const expectedEndpoint = { + ...ENDPOINT, + platform: "gcfv2", + httpsTrigger: {}, + }; + delete expectedEndpoint.runServiceId; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + serviceConfig: undefined, + }), + ).to.deep.equal(expectedEndpoint); + }); + }); + + describe("listFunctions", () => { + it("should pass back an error with the correct status", async () => { + nock(functionsV2Origin()) + .get("/v2/projects/foo/locations/-/functions") + .query({ filter: `environment="GEN_2"` }) + .reply(403, { error: "You don't have permissions." }); + + let errCaught = false; + try { + await cloudfunctionsv2.listFunctions("foo", "-"); + } catch (err: unknown) { + errCaught = true; + expect(err).instanceOf(FirebaseError); + expect(err).has.property("status", 403); + } + + expect(errCaught, "should have caught an error").to.be.true; + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index 2117106893b..82f14bce6fb 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -1,44 +1,53 @@ -import * as clc from "cli-color"; - -import { Client } from "../apiv2"; +import { Client, ClientVerbOptions } from "../apiv2"; import { FirebaseError } from "../error"; import { functionsV2Origin } from "../api"; import { logger } from "../logger"; +import { AUTH_BLOCKING_EVENTS } from "../functions/events/v1"; +import { PUBSUB_PUBLISH_EVENT } from "../functions/events/v2"; import * as backend from "../deploy/functions/backend"; -import * as runtimes from "../deploy/functions/runtimes"; +import * as supported from "../deploy/functions/runtimes/supported"; import * as proto from "./proto"; import * as utils from "../utils"; +import * as projectConfig from "../functions/projectConfig"; +import { + BLOCKING_EVENT_TO_LABEL_KEY, + BLOCKING_LABEL, + BLOCKING_LABEL_KEY_TO_EVENT, + CODEBASE_LABEL, + HASH_LABEL, +} from "../functions/constants"; +import { RequireKeys } from "../metaprogramming"; + +export const API_VERSION = "v2"; -export const API_VERSION = "v2alpha"; +// Defined by Cloud Run: https://cloud.google.com/run/docs/configuring/max-instances#setting +const DEFAULT_MAX_INSTANCE_COUNT = 100; const client = new Client({ - urlPrefix: functionsV2Origin, + urlPrefix: functionsV2Origin(), auth: true, apiVersion: API_VERSION, }); -export const PUBSUB_PUBLISH_EVENT = "google.cloud.pubsub.topic.v1.messagePublished"; - export type VpcConnectorEgressSettings = "PRIVATE_RANGES_ONLY" | "ALL_TRAFFIC"; export type IngressSettings = "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB"; export type FunctionState = "ACTIVE" | "FAILED" | "DEPLOYING" | "DELETING" | "UNKONWN"; -// The GCFv2 funtion type has many inner types which themselves have output-only fields: -// eventTrigger.trigger -// buildConfig.config -// buildConfig.workerPool -// serviceConfig.service -// serviceConfig.uri -// -// Because Omit<> doesn't work with nested property addresses, we're making those fields optional. -// An alternative would be to name the types OutputCloudFunction/CloudFunction or CloudFunction/InputCloudFunction. -export type OutputOnlyFields = "state" | "updateTime"; +// Values allowed for the operator field in EventFilter +export type EventFilterOperator = "match-path-pattern"; + +// Values allowed for the event trigger retry policy in case of a function's execution failure. +export type RetryPolicy = + | "RETRY_POLICY_UNSPECIFIED" + | "RETRY_POLICY_DO_NOT_RETRY" + | "RETRY_POLICY_RETRY"; /** Settings for building a container out of the customer source. */ export interface BuildConfig { - runtime: runtimes.Runtime; + runtime: supported.Runtime; entryPoint: string; source: Source; + sourceToken?: string; environmentVariables: Record; // Output only @@ -76,6 +85,21 @@ export interface Source { export interface EventFilter { attribute: string; value: string; + operator?: EventFilterOperator; +} + +/** + * Configurations for secret environment variables attached to a cloud functions resource. + */ +export interface SecretEnvVar { + /* Name of the environment variable. */ + key: string; + /* Project identifier (or project number) of the project that contains the secret. */ + projectId: string; + /* Name of the secret in secret manager. e.g. MY_SECRET, NOT projects/abc/secrets/MY_SECRET */ + secret: string; + /* Version of the secret (version number or the string 'latest') */ + version?: string; } /** The Cloud Run service that underlies a Cloud Function. */ @@ -87,19 +111,22 @@ export interface ServiceConfig { // cloudfunctions.net URLs. uri?: string; - timeoutSeconds?: number; - availableMemory?: string; - environmentVariables?: Record; - maxInstanceCount?: number; - minInstanceCount?: number; - vpcConnector?: string; - vpcConnectorEgressSettings?: VpcConnectorEgressSettings; - ingressSettings?: IngressSettings; + timeoutSeconds?: number | null; + availableMemory?: string | null; + availableCpu?: string | null; + environmentVariables?: Record | null; + secretEnvironmentVariables?: SecretEnvVar[] | null; + maxInstanceCount?: number | null; + minInstanceCount?: number | null; + maxInstanceRequestConcurrency?: number | null; + vpcConnector?: string | null; + vpcConnectorEgressSettings?: VpcConnectorEgressSettings | null; + ingressSettings?: IngressSettings | null; // The service account for default credentials. Defaults to the // default compute account. This is different from the v1 default // of the default GAE account. - serviceAccountEmail?: string; + serviceAccountEmail?: string | null; } export interface EventTrigger { @@ -118,18 +145,33 @@ export interface EventTrigger { // run.routes.invoke permission on the target service. Defaults // to the defualt compute service account. serviceAccountEmail?: string; + + retryPolicy?: RetryPolicy; + + // The name of the channel associated with the trigger in + // `projects/{project}/locations/{location}/channels/{channel}` format. + channel?: string; } -export interface CloudFunction { +interface CloudFunctionBase { name: string; description?: string; buildConfig: BuildConfig; - serviceConfig: ServiceConfig; + serviceConfig?: ServiceConfig; eventTrigger?: EventTrigger; + labels?: Record | null; +} + +export type OutputCloudFunction = CloudFunctionBase & { state: FunctionState; updateTime: Date; - labels?: Record; -} + serviceConfig?: RequireKeys; +}; + +export type InputCloudFunction = CloudFunctionBase & { + // serviceConfig is required. + serviceConfig: ServiceConfig; +}; export interface OperationMetadata { createTime: string; @@ -148,13 +190,13 @@ export interface Operation { metadata?: OperationMetadata; done: boolean; error?: { code: number; message: string; details: unknown }; - response?: CloudFunction; + response?: OutputCloudFunction; } // Private API interface for ListFunctionsResponse. listFunctions returns // a CloudFunction[] interface ListFunctionsResponse { - functions: CloudFunction[]; + functions: OutputCloudFunction[]; unreachable: string[]; } @@ -183,7 +225,7 @@ const BYTES_PER_UNIT: Record = { * Must serve the same results as * https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go */ -export function megabytes(memory: string): number { +export function mebibytes(memory: string): number { const re = /^([0-9]+(\.[0-9]*)?)(Ki|Mi|Gi|Ti|k|M|G|T|([eE]([0-9]+)))?$/; const matches = re.exec(memory); if (!matches) { @@ -197,30 +239,58 @@ export function megabytes(memory: string): number { const suffix = matches[3] || ""; bytes = quantity * BYTES_PER_UNIT[suffix as MemoryUnit]; } - return bytes / 1e6; + return bytes / (1 << 20); } /** * Logs an error from a failed function deployment. - * @param funcName Name of the function that was unsuccessfully deployed. + * @param func The function that was unsuccessfully deployed. * @param type Type of deployment - create, update, or delete. * @param err The error returned from the operation. */ -function functionsOpLogReject(funcName: string, type: string, err: any): void { - if (err?.context?.response?.statusCode === 429) { - utils.logWarning( - `${clc.bold.yellow( - "functions:" - )} got "Quota Exceeded" error while trying to ${type} ${funcName}. Waiting to retry...` +function functionsOpLogReject(func: InputCloudFunction, type: string, err: any): void { + if (err?.message?.includes("maxScale may not exceed")) { + const maxInstances = func.serviceConfig.maxInstanceCount || DEFAULT_MAX_INSTANCE_COUNT; + utils.logLabeledWarning( + "functions", + `Your current project quotas don't allow for the current max instances setting of ${maxInstances}. ` + + "Either reduce this function's maximum instances, or request a quota increase on the underlying Cloud Run service " + + "at https://cloud.google.com/run/quotas.", + ); + const suggestedFix = func.buildConfig.runtime.startsWith("python") + ? "firebase_functions.options.set_global_options(max_instances=10)" + : "setGlobalOptions({maxInstances: 10})"; + utils.logLabeledWarning( + "functions", + `You can adjust the max instances value in your function's runtime options:\n\t${suggestedFix}`, ); } else { - utils.logWarning( - clc.bold.yellow("functions:") + " failed to " + type + " function " + funcName + utils.logLabeledWarning("functions", `${err?.message}`); + if (err?.context?.response?.statusCode === 429) { + utils.logLabeledWarning( + "functions", + `Got "Quota Exceeded" error while trying to ${type} ${func.name}. Waiting to retry...`, + ); + } else if ( + err?.message?.includes( + "If you recently started to use Eventarc, it may take a few minutes before all necessary permissions are propagated to the Service Agent", + ) + ) { + utils.logLabeledWarning( + "functions", + `Since this is your first time using 2nd gen functions, we need a little bit longer to finish setting everything up. Retry the deployment in a few minutes.`, + ); + } + utils.logLabeledWarning( + "functions", + + ` failed to ${type} function ${func.name}`, ); } - throw new FirebaseError(`Failed to ${type} function ${funcName}`, { + throw new FirebaseError(`Failed to ${type} function ${func.name}`, { original: err, - context: { function: funcName }, + status: err?.context?.response?.statusCode, + context: { function: func.name }, }); } @@ -229,16 +299,16 @@ function functionsOpLogReject(funcName: string, type: string, err: any): void { */ export async function generateUploadUrl( projectId: string, - location: string + location: string, ): Promise { try { const res = await client.post( - `projects/${projectId}/locations/${location}/functions:generateUploadUrl` + `projects/${projectId}/locations/${location}/functions:generateUploadUrl`, ); return res.body; - } catch (err) { + } catch (err: any) { logger.info( - "\n\nThere was an issue deploying your functions. Verify that your project has a Google App Engine instance setup at https://console.cloud.google.com/appengine and try again. If this issue persists, please contact support." + "\n\nThere was an issue deploying your functions. Verify that your project has a Google App Engine instance setup at https://console.cloud.google.com/appengine and try again. If this issue persists, please contact support.", ); throw err; } @@ -247,22 +317,32 @@ export async function generateUploadUrl( /** * Creates a new Cloud Function. */ -export async function createFunction( - cloudFunction: Omit -): Promise { +export async function createFunction(cloudFunction: InputCloudFunction): Promise { // the API is a POST to the collection that owns the function name. const components = cloudFunction.name.split("/"); const functionId = components.splice(-1, 1)[0]; + cloudFunction.buildConfig.environmentVariables = { + ...cloudFunction.buildConfig.environmentVariables, + // Disable GCF from automatically running npm run build script + // https://cloud.google.com/functions/docs/release-notes + GOOGLE_NODE_RUN_SCRIPTS: "", + }; + + cloudFunction.serviceConfig.environmentVariables = { + ...cloudFunction.serviceConfig.environmentVariables, + FUNCTION_TARGET: cloudFunction.buildConfig.entryPoint.replaceAll("-", "."), + }; + try { const res = await client.post( components.join("/"), cloudFunction, - { queryParams: { functionId } } + { queryParams: { functionId } }, ); return res.body; - } catch (err) { - throw functionsOpLogReject(cloudFunction.name, "create", err); + } catch (err: any) { + throw functionsOpLogReject(cloudFunction, "create", err); } } @@ -272,10 +352,10 @@ export async function createFunction( export async function getFunction( projectId: string, location: string, - functionId: string -): Promise { + functionId: string, +): Promise { const name = `projects/${projectId}/locations/${location}/functions/${functionId}`; - const res = await client.get(name); + const res = await client.get(name); return res.body; } @@ -283,9 +363,12 @@ export async function getFunction( * List all functions in a region. * Customers should generally use backend.existingBackend. */ -export async function listFunctions(projectId: string, region: string): Promise { +export async function listFunctions( + projectId: string, + region: string, +): Promise { const res = await listFunctionsInternal(projectId, region); - if (res.unreachable!.includes(region)) { + if (res.unreachable.includes(region)) { throw new FirebaseError(`Cloud Functions region ${region} is unavailable`); } return res.functions; @@ -301,15 +384,19 @@ export async function listAllFunctions(projectId: string): Promise { type Response = ListFunctionsResponse & { nextPageToken?: string }; - const functions: CloudFunction[] = []; + const functions: OutputCloudFunction[] = []; const unreacahble = new Set(); let pageToken = ""; while (true) { const url = `projects/${projectId}/locations/${region}/functions`; - const opts = pageToken == "" ? {} : { queryParams: { pageToken } }; + // V2 API returns both V1 and V2 Functions. Add filter condition to return only V2 functions. + const opts: ClientVerbOptions = { queryParams: { filter: `environment="GEN_2"` } }; + if (pageToken !== "") { + opts.queryParams = { ...opts.queryParams, pageToken }; + } const res = await client.get(url, opts); functions.push(...(res.body.functions || [])); for (const region of res.body.unreachable || []) { @@ -330,21 +417,41 @@ async function listFunctionsInternal( * Updates a Cloud Function. * Customers can force a field to be deleted by setting that field to `undefined` */ -export async function updateFunction( - cloudFunction: Omit -): Promise { +export async function updateFunction(cloudFunction: InputCloudFunction): Promise { + // Keys in labels and environmentVariables and secretEnvironmentVariables are user defined, so we don't recurse + // for field masks. + const fieldMasks = proto.fieldMasks( + cloudFunction, + /* doNotRecurseIn...=*/ "labels", + "serviceConfig.environmentVariables", + "serviceConfig.secretEnvironmentVariables", + ); + + cloudFunction.buildConfig.environmentVariables = { + ...cloudFunction.buildConfig.environmentVariables, + // Disable GCF from automatically running npm run build script + // https://cloud.google.com/functions/docs/release-notes + GOOGLE_NODE_RUN_SCRIPTS: "", + }; + fieldMasks.push("buildConfig.buildEnvironmentVariables"); + + cloudFunction.serviceConfig.environmentVariables = { + ...cloudFunction.serviceConfig.environmentVariables, + FUNCTION_TARGET: cloudFunction.buildConfig.entryPoint.replaceAll("-", "."), + }; + try { const queryParams = { - updateMask: proto.fieldMasks(cloudFunction).join(","), + updateMask: fieldMasks.join(","), }; const res = await client.patch( cloudFunction.name, cloudFunction, - { queryParams } + { queryParams }, ); return res.body; - } catch (err) { - throw functionsOpLogReject(cloudFunction.name, "update", err); + } catch (err: any) { + throw functionsOpLogReject(cloudFunction, "update", err); } } @@ -356,32 +463,35 @@ export async function deleteFunction(cloudFunction: string): Promise try { const res = await client.delete(cloudFunction); return res.body; - } catch (err) { - throw functionsOpLogReject(cloudFunction, "update", err); + } catch (err: any) { + throw functionsOpLogReject({ name: cloudFunction } as InputCloudFunction, "update", err); } } -export function functionFromEndpoint(endpoint: backend.Endpoint, source: StorageSource) { - if (endpoint.platform != "gcfv2") { +/** + * Generate a v2 Cloud Function API object from a versionless Endpoint object. + */ +export function functionFromEndpoint(endpoint: backend.Endpoint): InputCloudFunction { + if (endpoint.platform !== "gcfv2") { throw new FirebaseError( - "Trying to create a v2 CloudFunction with v1 API. This should never happen" + "Trying to create a v2 CloudFunction with v1 API. This should never happen", ); } - if (!runtimes.isValidRuntime(endpoint.runtime)) { + if (!supported.isRuntime(endpoint.runtime)) { throw new FirebaseError( "Failed internal assertion. Trying to deploy a new function with a deprecated runtime." + - " This should never happen" + " This should never happen", ); } - const gcfFunction: Omit = { + const gcfFunction: InputCloudFunction = { name: backend.functionName(endpoint), buildConfig: { runtime: endpoint.runtime, entryPoint: endpoint.entryPoint, source: { - storageSource: source, + storageSource: endpoint.source?.storageSource, }, // We don't use build environment variables, environmentVariables: {}, @@ -394,61 +504,141 @@ export function functionFromEndpoint(endpoint: backend.Endpoint, source: Storage gcfFunction.serviceConfig, endpoint, "environmentVariables", - "vpcConnector", - "vpcConnectorEgressSettings", - "serviceAccountEmail", - "ingressSettings" + "secretEnvironmentVariables", + "ingressSettings", + "timeoutSeconds", ); proto.renameIfPresent( gcfFunction.serviceConfig, endpoint, - "availableMemory", - "availableMemoryMb", - (mb: string) => `${mb}M` + "serviceAccountEmail", + "serviceAccount", ); + // Memory must be set because the default value of GCF gen 2 is Megabytes and + // we use mebibytes + const mem = endpoint.availableMemoryMb || backend.DEFAULT_MEMORY; + gcfFunction.serviceConfig.availableMemory = mem > 1024 ? `${mem / 1024}Gi` : `${mem}Mi`; + proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "minInstanceCount", "minInstances"); + proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "maxInstanceCount", "maxInstances"); + // N.B. only convert CPU and concurrency fields for 2nd gen functions, once we + // eventually use the v2 API to configure both 1st and 2nd gen functions) proto.renameIfPresent( gcfFunction.serviceConfig, endpoint, - "timeoutSeconds", - "timeout", - proto.secondsFromDuration + "maxInstanceRequestConcurrency", + "concurrency", ); - proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "minInstanceCount", "minInstances"); - proto.renameIfPresent(gcfFunction.serviceConfig, endpoint, "maxInstanceCount", "maxInstances"); + proto.convertIfPresent(gcfFunction.serviceConfig, endpoint, "availableCpu", "cpu", (cpu) => { + return String(cpu); + }); + + if (endpoint.vpc) { + proto.renameIfPresent(gcfFunction.serviceConfig, endpoint.vpc, "vpcConnector", "connector"); + proto.renameIfPresent( + gcfFunction.serviceConfig, + endpoint.vpc, + "vpcConnectorEgressSettings", + "egressSettings", + ); + } else if (endpoint.vpc === null) { + gcfFunction.serviceConfig.vpcConnector = null; + gcfFunction.serviceConfig.vpcConnectorEgressSettings = null; + } if (backend.isEventTriggered(endpoint)) { gcfFunction.eventTrigger = { eventType: endpoint.eventTrigger.eventType, + retryPolicy: "RETRY_POLICY_UNSPECIFIED", }; + if (endpoint.serviceAccount) { + gcfFunction.eventTrigger.serviceAccountEmail = endpoint.serviceAccount; + } if (gcfFunction.eventTrigger.eventType === PUBSUB_PUBLISH_EVENT) { - gcfFunction.eventTrigger.pubsubTopic = endpoint.eventTrigger.eventFilters.resource; - } else { + if (!endpoint.eventTrigger.eventFilters?.topic) { + throw new FirebaseError( + "Error: Pub/Sub event trigger is missing topic: " + + JSON.stringify(endpoint.eventTrigger, null, 2), + ); + } + gcfFunction.eventTrigger.pubsubTopic = endpoint.eventTrigger.eventFilters.topic; gcfFunction.eventTrigger.eventFilters = []; for (const [attribute, value] of Object.entries(endpoint.eventTrigger.eventFilters)) { + if (attribute === "topic") continue; + gcfFunction.eventTrigger.eventFilters.push({ attribute, value }); + } + } else { + gcfFunction.eventTrigger.eventFilters = []; + for (const [attribute, value] of Object.entries(endpoint.eventTrigger.eventFilters || {})) { gcfFunction.eventTrigger.eventFilters.push({ attribute, value }); } + for (const [attribute, value] of Object.entries( + endpoint.eventTrigger.eventFilterPathPatterns || {}, + )) { + gcfFunction.eventTrigger.eventFilters.push({ + attribute, + value, + operator: "match-path-pattern", + }); + } } proto.renameIfPresent( gcfFunction.eventTrigger, endpoint.eventTrigger, "triggerRegion", - "region" + "region", ); - - if (endpoint.eventTrigger.retry) { - logger.warn("Cannot set a retry policy on Cloud Function", endpoint.id); - } + proto.copyIfPresent(gcfFunction.eventTrigger, endpoint.eventTrigger, "channel"); + + endpoint.eventTrigger.retry + ? (gcfFunction.eventTrigger.retryPolicy = "RETRY_POLICY_RETRY") + : (gcfFunction.eventTrigger.retryPolicy = "RETRY_POLICY_DO_NOT_RETRY"); + + // By default, Functions Framework in GCFv2 opts to downcast incoming cloudevent messages to legacy formats. + // Since Firebase Functions SDK expects messages in cloudevent format, we set FUNCTION_SIGNATURE_TYPE to tell + // Functions Framework to disable downcast before passing the cloudevent message to function handler. + // See https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/master/README.md#configure-the-functions- + gcfFunction.serviceConfig.environmentVariables = { + ...gcfFunction.serviceConfig.environmentVariables, + FUNCTION_SIGNATURE_TYPE: "cloudevent", + }; } else if (backend.isScheduleTriggered(endpoint)) { // trigger type defaults to HTTPS. gcfFunction.labels = { ...gcfFunction.labels, "deployment-scheduled": "true" }; } else if (backend.isTaskQueueTriggered(endpoint)) { gcfFunction.labels = { ...gcfFunction.labels, "deployment-taskqueue": "true" }; + } else if (backend.isCallableTriggered(endpoint)) { + gcfFunction.labels = { ...gcfFunction.labels, "deployment-callable": "true" }; + } else if (backend.isBlockingTriggered(endpoint)) { + gcfFunction.labels = { + ...gcfFunction.labels, + [BLOCKING_LABEL]: + BLOCKING_EVENT_TO_LABEL_KEY[ + endpoint.blockingTrigger.eventType as (typeof AUTH_BLOCKING_EVENTS)[number] + ], + }; + } + const codebase = endpoint.codebase || projectConfig.DEFAULT_CODEBASE; + if (codebase !== projectConfig.DEFAULT_CODEBASE) { + gcfFunction.labels = { + ...gcfFunction.labels, + [CODEBASE_LABEL]: codebase, + }; + } else { + delete gcfFunction.labels?.[CODEBASE_LABEL]; + } + if (endpoint.hash) { + gcfFunction.labels = { + ...gcfFunction.labels, + [HASH_LABEL]: endpoint.hash, + }; } - return gcfFunction; } -export function endpointFromFunction(gcfFunction: CloudFunction): backend.Endpoint { +/** + * Generate a versionless Endpoint object from a v2 Cloud Function API object. + */ +export function endpointFromFunction(gcfFunction: OutputCloudFunction): backend.Endpoint { const [, project, , region, , id] = gcfFunction.name.split("/"); let trigger: backend.Triggered; if (gcfFunction.labels?.["deployment-scheduled"] === "true") { @@ -459,32 +649,57 @@ export function endpointFromFunction(gcfFunction: CloudFunction): backend.Endpoi trigger = { taskQueueTrigger: {}, }; - } else if (gcfFunction.eventTrigger) { + } else if (gcfFunction.labels?.["deployment-callable"] === "true") { trigger = { - eventTrigger: { - eventType: gcfFunction.eventTrigger!.eventType, - eventFilters: {}, - retry: false, + callableTrigger: {}, + }; + } else if (gcfFunction.labels?.[BLOCKING_LABEL]) { + trigger = { + blockingTrigger: { + eventType: BLOCKING_LABEL_KEY_TO_EVENT[gcfFunction.labels[BLOCKING_LABEL]], }, }; - if (gcfFunction.eventTrigger.pubsubTopic) { - trigger.eventTrigger.eventFilters.resource = gcfFunction.eventTrigger.pubsubTopic; + } else if (gcfFunction.eventTrigger) { + const eventFilters: Record = {}; + const eventFilterPathPatterns: Record = {}; + if ( + gcfFunction.eventTrigger.pubsubTopic && + gcfFunction.eventTrigger.eventType === PUBSUB_PUBLISH_EVENT + ) { + eventFilters.topic = gcfFunction.eventTrigger.pubsubTopic; } else { - for (const { attribute, value } of gcfFunction.eventTrigger.eventFilters || []) { - trigger.eventTrigger.eventFilters[attribute] = value; + for (const eventFilter of gcfFunction.eventTrigger.eventFilters || []) { + if (eventFilter.operator === "match-path-pattern") { + eventFilterPathPatterns[eventFilter.attribute] = eventFilter.value; + } else { + eventFilters[eventFilter.attribute] = eventFilter.value; + } } } + trigger = { + eventTrigger: { + eventType: gcfFunction.eventTrigger.eventType, + retry: gcfFunction.eventTrigger.retryPolicy === "RETRY_POLICY_RETRY" ? true : false, + }, + }; + if (Object.keys(eventFilters).length) { + trigger.eventTrigger.eventFilters = eventFilters; + } + if (Object.keys(eventFilterPathPatterns).length) { + trigger.eventTrigger.eventFilterPathPatterns = eventFilterPathPatterns; + } + proto.copyIfPresent(trigger.eventTrigger, gcfFunction.eventTrigger, "channel"); proto.renameIfPresent( trigger.eventTrigger, gcfFunction.eventTrigger, "region", - "triggerRegion" + "triggerRegion", ); } else { trigger = { httpsTrigger: {} }; } - if (!runtimes.isValidRuntime(gcfFunction.buildConfig.runtime)) { + if (!supported.isRuntime(gcfFunction.buildConfig.runtime)) { logger.debug("GCFv2 function has a deprecated runtime:", JSON.stringify(gcfFunction, null, 2)); } @@ -496,34 +711,79 @@ export function endpointFromFunction(gcfFunction: CloudFunction): backend.Endpoi ...trigger, entryPoint: gcfFunction.buildConfig.entryPoint, runtime: gcfFunction.buildConfig.runtime, - uri: gcfFunction.serviceConfig.uri, + source: gcfFunction.buildConfig.source, }; - proto.copyIfPresent( - endpoint, - gcfFunction.serviceConfig, - "serviceAccountEmail", - "vpcConnector", - "vpcConnectorEgressSettings", - "ingressSettings", - "environmentVariables" - ); - proto.renameIfPresent( - endpoint, - gcfFunction.serviceConfig, - "availableMemoryMb", - "availableMemory", - megabytes - ); - proto.renameIfPresent( - endpoint, - gcfFunction.serviceConfig, - "timeout", - "timeoutSeconds", - proto.durationFromSeconds - ); - proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "minInstances", "minInstanceCount"); - proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "maxInstances", "maxInstanceCount"); - proto.copyIfPresent(endpoint, gcfFunction, "labels"); - + if (gcfFunction.serviceConfig) { + proto.copyIfPresent( + endpoint, + gcfFunction.serviceConfig, + "ingressSettings", + "environmentVariables", + "secretEnvironmentVariables", + "timeoutSeconds", + "uri", + ); + proto.renameIfPresent( + endpoint, + gcfFunction.serviceConfig, + "serviceAccount", + "serviceAccountEmail", + ); + proto.convertIfPresent( + endpoint, + gcfFunction.serviceConfig, + "availableMemoryMb", + "availableMemory", + (prod) => { + if (prod === null) { + logger.debug("Prod should always return a valid memory amount"); + return prod as never; + } + const mem = mebibytes(prod); + if (!backend.isValidMemoryOption(mem)) { + logger.debug("Converting a function to an endpoint with an invalid memory option", mem); + } + return mem as backend.MemoryOptions; + }, + ); + proto.convertIfPresent(endpoint, gcfFunction.serviceConfig, "cpu", "availableCpu", (cpu) => { + let cpuVal: number | null = Number(cpu); + if (Number.isNaN(cpuVal)) { + cpuVal = null; + } + return cpuVal; + }); + proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "minInstances", "minInstanceCount"); + proto.renameIfPresent(endpoint, gcfFunction.serviceConfig, "maxInstances", "maxInstanceCount"); + proto.renameIfPresent( + endpoint, + gcfFunction.serviceConfig, + "concurrency", + "maxInstanceRequestConcurrency", + ); + proto.copyIfPresent(endpoint, gcfFunction, "labels"); + if (gcfFunction.serviceConfig.vpcConnector) { + endpoint.vpc = { connector: gcfFunction.serviceConfig.vpcConnector }; + proto.renameIfPresent( + endpoint.vpc, + gcfFunction.serviceConfig, + "egressSettings", + "vpcConnectorEgressSettings", + ); + } + const serviceName = gcfFunction.serviceConfig.service; + if (!serviceName) { + logger.debug( + "Got a v2 function without a service name." + + "Maybe we've migrated to using the v2 API everywhere and missed this code", + ); + } else { + endpoint.runServiceId = utils.last(serviceName.split("/")); + } + } + endpoint.codebase = gcfFunction.labels?.[CODEBASE_LABEL] || projectConfig.DEFAULT_CODEBASE; + if (gcfFunction.labels?.[HASH_LABEL]) { + endpoint.hash = gcfFunction.labels[HASH_LABEL]; + } return endpoint; } diff --git a/src/gcp/cloudlogging.ts b/src/gcp/cloudlogging.ts index fdc773d1c43..9294dfbc0a5 100644 --- a/src/gcp/cloudlogging.ts +++ b/src/gcp/cloudlogging.ts @@ -1,4 +1,5 @@ -import * as api from "../api"; +import { cloudloggingOrigin } from "../api"; +import { Client } from "../apiv2"; import { FirebaseError } from "../error"; const API_VERSION = "v2"; @@ -30,22 +31,21 @@ export async function listEntries( projectId: string, filter: string, pageSize: number, - order: string + order: string, ): Promise { - const endpoint = `/${API_VERSION}/entries:list`; + const client = new Client({ urlPrefix: cloudloggingOrigin(), apiVersion: API_VERSION }); try { - const result = await api.request("POST", endpoint, { - auth: true, - data: { - resourceNames: [`projects/${projectId}`], - filter: filter, - orderBy: "timestamp " + order, - pageSize: pageSize, - }, - origin: api.cloudloggingOrigin, + const result = await client.post< + { resourceNames: string[]; filter: string; orderBy: string; pageSize: number }, + { entries: LogEntry[] } + >("/entries:list", { + resourceNames: [`projects/${projectId}`], + filter: filter, + orderBy: `timestamp ${order}`, + pageSize: pageSize, }); return result.body.entries; - } catch (err) { + } catch (err: any) { throw new FirebaseError("Failed to retrieve log entries from Google Cloud.", { original: err, }); diff --git a/src/gcp/cloudmonitoring.spec.ts b/src/gcp/cloudmonitoring.spec.ts new file mode 100644 index 00000000000..70fd987a3bd --- /dev/null +++ b/src/gcp/cloudmonitoring.spec.ts @@ -0,0 +1,48 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as api from "../api"; +import { FirebaseError } from "../error"; +import { Aligner, CmQuery, queryTimeSeries, TimeSeriesView } from "./cloudmonitoring"; + +const CLOUD_MONITORING_VERSION = "v3"; +const PROJECT_NUMBER = 1; + +describe("queryTimeSeries", () => { + afterEach(() => { + nock.cleanAll(); + }); + + const query: CmQuery = { + filter: + 'metric.type="firebaseextensions.googleapis.com/extension/version/active_instances" resource.type="firebaseextensions.googleapis.com/ExtensionVersion"', + "interval.endTime": new Date().toJSON(), + "interval.startTime": new Date().toJSON(), + view: TimeSeriesView.FULL, + "aggregation.alignmentPeriod": (60 * 60 * 24).toString() + "s", + "aggregation.perSeriesAligner": Aligner.ALIGN_MAX, + }; + + const RESPONSE = { + timeSeries: [], + }; + + it("should make a POST call to the correct endpoint", async () => { + nock(api.cloudMonitoringOrigin()) + .get(`/${CLOUD_MONITORING_VERSION}/projects/${PROJECT_NUMBER}/timeSeries/`) + .query(true) + .reply(200, RESPONSE); + + const res = await queryTimeSeries(query, PROJECT_NUMBER); + expect(res).to.deep.equal(RESPONSE.timeSeries); + expect(nock.isDone()).to.be.true; + }); + + it("should throw a FirebaseError if the endpoint returns an error response", async () => { + nock(api.cloudMonitoringOrigin()) + .get(`/${CLOUD_MONITORING_VERSION}/projects/${PROJECT_NUMBER}/timeSeries/`) + .query(true) + .reply(404); + await expect(queryTimeSeries(query, PROJECT_NUMBER)).to.be.rejectedWith(FirebaseError); + expect(nock.isDone()).to.be.true; + }); +}); diff --git a/src/gcp/cloudmonitoring.ts b/src/gcp/cloudmonitoring.ts new file mode 100644 index 00000000000..99e2a06b43f --- /dev/null +++ b/src/gcp/cloudmonitoring.ts @@ -0,0 +1,154 @@ +import { cloudMonitoringOrigin } from "../api"; +import { Client } from "../apiv2"; +import { FirebaseError } from "../error"; + +export const CLOUD_MONITORING_VERSION = "v3"; + +/** + * Content of this file is borrowed from Cloud monitoring console's source code. + * https://source.corp.google.com/piper///depot/google3/java/com/google/firebase/console/web/components/cloud_monitoring/typedefs.ts + */ + +/** Query from v3 Cloud Monitoring API */ +export interface CmQuery { + filter: string; + "interval.startTime"?: string; + "interval.endTime"?: string; + "aggregation.alignmentPeriod"?: string; + "aggregation.perSeriesAligner"?: Aligner; + "aggregation.crossSeriesReducer"?: Reducer; + "aggregation.groupByFields"?: string; + orderBy?: string; + pageSize?: number; + pageToken?: string; + view?: TimeSeriesView; +} + +/** + * Controls which fields are returned by ListTimeSeries. + */ +export enum TimeSeriesView { + FULL = "FULL", + HEADERS = "HEADERS", +} + +/** + * The Aligner describes how to bring the data points in a single time series + * into temporal alignment. + */ +export enum Aligner { + ALIGN_NONE = "ALIGN_NONE", + ALIGN_DELTA = "ALIGN_DELTA", + ALIGN_RATE = "ALIGN_RATE", + ALIGN_INTERPOLATE = "ALIGN_INTERPOLATE", + ALIGN_NEXT_OLDER = "ALIGN_NEXT_OLDER", + ALIGN_MIN = "ALIGN_MIN", + ALIGN_MAX = "ALIGN_MAX", + ALIGN_MEAN = "ALIGN_MEAN", + ALIGN_COUNT = "ALIGN_COUNT", + ALIGN_SUM = "ALIGN_SUM", + ALIGN_STDDEV = "ALIGN_STDDEV", + ALIGN_COUNT_TRUE = "ALIGN_COUNT_TRUE", + ALIGN_FRACTION_TRUE = "ALIGN_FRACTION_TRUE", +} + +export enum MetricKind { + METRIC_KIND_UNSPECIFIED = "METRIC_KIND_UNSPECIFIED", + GAUGE = "GAUGE", + DELTA = "DELTA", + CUMULATIVE = "CUMULATIVE", +} + +/** + * A Reducer describes how to aggregate data points from multiple time series + * into a single time series. + */ +export enum Reducer { + REDUCE_NONE = "REDUCE_NONE", + REDUCE_MEAN = "REDUCE_MEAN", + REDUCE_MIN = "REDUCE_MIN", + REDUCE_MAX = "REDUCE_MAX", + REDUCE_SUM = "REDUCE_SUM", + REDUCE_STDDEV = "REDUCE_STDDEV", + REDUCE_COUNT = "REDUCE_COUNT", + REDUCE_COUNT_TRUE = "REDUCE_COUNT_TRUE", + REDUCE_FRACTION_TRUE = "REDUCE_FRACTION_TRUE", + REDUCE_PERCENTILE_99 = "REDUCE_PERCENTILE_99", + REDUCE_PERCENTILE_95 = "REDUCE_PERCENTILE_95", + REDUCE_PERCENTILE_50 = "REDUCE_PERCENTILE_50", + REDUCE_PERCENTILE_05 = "REDUCE_PERCENTILE_05", +} + +/** TimeSeries from v3 Cloud Monitoring API */ +export interface TimeSeries { + metric: Metric; + metricKind: MetricKind; + points: Point[]; + resource: Resource; + valueType: ValueType; +} +export type TimeSeriesResponse = TimeSeries[]; + +/** Resource from v3 Cloud Monitoring API */ +export interface Resource { + labels: { [key: string]: string }; + type: string; +} +export type Metric = Resource; + +/** Point from v3 Cloud Monitoring API */ +export interface Point { + interval: Interval; + value: TypedValue; +} + +/** Interval from v3 Cloud Monitoring API */ +export interface Interval { + endTime: string; + startTime: string; +} + +/** TypedValue from v3 Cloud Monitoring API */ +export interface TypedValue { + boolValue?: boolean; + int64Value?: number; + doubleValue?: number; + stringValue?: string; +} + +/** + * The value type of a metric. + */ +export enum ValueType { + VALUE_TYPE_UNSPECIFIED = "VALUE_TYPE_UNSPECIFIED", + BOOL = "BOOL", + INT64 = "INT64", + DOUBLE = "DOUBLE", + STRING = "STRING", +} + +/** + * Get usage metrics for all extensions from Cloud Monitoring API. + */ +export async function queryTimeSeries( + query: CmQuery, + projectNumber: number, +): Promise { + const client = new Client({ + urlPrefix: cloudMonitoringOrigin(), + apiVersion: CLOUD_MONITORING_VERSION, + }); + try { + const res = await client.get<{ timeSeries: TimeSeriesResponse }>( + `/projects/${projectNumber}/timeSeries/`, + { + queryParams: query as { [key: string]: any }, + }, + ); + return res.body.timeSeries; + } catch (err: any) { + throw new FirebaseError(`Failed to get extension usage: ${err}`, { + status: err.status, + }); + } +} diff --git a/src/gcp/cloudscheduler.spec.ts b/src/gcp/cloudscheduler.spec.ts new file mode 100644 index 00000000000..124fee94d98 --- /dev/null +++ b/src/gcp/cloudscheduler.spec.ts @@ -0,0 +1,287 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import { FirebaseError } from "../error"; +import * as api from "../api"; +import * as backend from "../deploy/functions/backend"; +import * as cloudscheduler from "./cloudscheduler"; +import { cloneDeep } from "../utils"; + +const VERSION = "v1"; + +const TEST_JOB: cloudscheduler.Job = { + name: "projects/test-project/locations/us-east1/jobs/test", + schedule: "every 5 minutes", + timeZone: "America/Los_Angeles", + pubsubTarget: { + topicName: "projects/test-project/topics/test", + attributes: { + scheduled: "true", + }, + }, + retryConfig: {}, +}; + +describe("cloudscheduler", () => { + describe("createOrUpdateJob", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should create a job if none exists", async () => { + nock(api.cloudschedulerOrigin()) + .get(`/${VERSION}/${TEST_JOB.name}`) + .reply(404, { context: { response: { statusCode: 404 } } }); + nock(api.cloudschedulerOrigin()) + .post(`/${VERSION}/projects/test-project/locations/us-east1/jobs`) + .reply(200, TEST_JOB); + + const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); + + expect(response.body).to.deep.equal(TEST_JOB); + expect(nock.isDone()).to.be.true; + }); + + it("should do nothing if a functionally identical job exists", async () => { + const otherJob = cloneDeep(TEST_JOB); + otherJob.name = "something-different"; + nock(api.cloudschedulerOrigin()).get(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); + + const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); + + expect(response).to.be.undefined; + expect(nock.isDone()).to.be.true; + }); + + it("should do nothing if a job exists with superset retry config.", async () => { + const existingJob = cloneDeep(TEST_JOB); + existingJob.retryConfig = { maxDoublings: 10, retryCount: 2 }; + const newJob = cloneDeep(existingJob); + newJob.retryConfig = { maxDoublings: 10 }; + nock(api.cloudschedulerOrigin()) + .get(`/${VERSION}/${TEST_JOB.name}`) + .query(true) + .reply(200, existingJob); + + const response = await cloudscheduler.createOrReplaceJob(newJob); + + expect(response).to.be.undefined; + expect(nock.isDone()).to.be.true; + }); + + it("should update if a job exists with the same name and a different schedule", async () => { + const otherJob = cloneDeep(TEST_JOB); + otherJob.schedule = "every 6 minutes"; + nock(api.cloudschedulerOrigin()) + .get(`/${VERSION}/${TEST_JOB.name}`) + .query(true) + .reply(200, otherJob); + nock(api.cloudschedulerOrigin()) + .patch(`/${VERSION}/${TEST_JOB.name}`) + .query(true) + .reply(200, otherJob); + + const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); + + expect(response.body).to.deep.equal(otherJob); + expect(nock.isDone()).to.be.true; + }); + + it("should update if a job exists with the same name but a different timeZone", async () => { + const otherJob = cloneDeep(TEST_JOB); + otherJob.timeZone = "America/New_York"; + nock(api.cloudschedulerOrigin()) + .get(`/${VERSION}/${TEST_JOB.name}`) + .query(true) + .reply(200, otherJob); + nock(api.cloudschedulerOrigin()) + .patch(`/${VERSION}/${TEST_JOB.name}`) + .query(true) + .reply(200, otherJob); + + const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); + + expect(response.body).to.deep.equal(otherJob); + expect(nock.isDone()).to.be.true; + }); + + it("should update if a job exists with the same name but a different retry config", async () => { + const otherJob = cloneDeep(TEST_JOB); + otherJob.retryConfig = { maxDoublings: 10 }; + nock(api.cloudschedulerOrigin()) + .get(`/${VERSION}/${TEST_JOB.name}`) + .query(true) + .reply(200, TEST_JOB); + nock(api.cloudschedulerOrigin()) + .patch(`/${VERSION}/${TEST_JOB.name}`) + .query(true) + .reply(200, otherJob); + + const response = await cloudscheduler.createOrReplaceJob(otherJob); + + expect(response.body).to.deep.equal(otherJob); + expect(nock.isDone()).to.be.true; + }); + + it("should error and exit if cloud resource location is not set", async () => { + nock(api.cloudschedulerOrigin()) + .get(`/${VERSION}/${TEST_JOB.name}`) + .reply(404, { context: { response: { statusCode: 404 } } }); + nock(api.cloudschedulerOrigin()) + .post(`/${VERSION}/projects/test-project/locations/us-east1/jobs`) + .reply(404, { context: { response: { statusCode: 404 } } }); + + await expect(cloudscheduler.createOrReplaceJob(TEST_JOB)).to.be.rejectedWith( + FirebaseError, + "Cloud resource location is not set", + ); + + expect(nock.isDone()).to.be.true; + }); + + it("should error and exit if cloud scheduler create request fail", async () => { + nock(api.cloudschedulerOrigin()) + .get(`/${VERSION}/${TEST_JOB.name}`) + .reply(404, { context: { response: { statusCode: 404 } } }); + nock(api.cloudschedulerOrigin()) + .post(`/${VERSION}/projects/test-project/locations/us-east1/jobs`) + .reply(400, { context: { response: { statusCode: 400 } } }); + + await expect(cloudscheduler.createOrReplaceJob(TEST_JOB)).to.be.rejectedWith( + FirebaseError, + "Failed to create scheduler job projects/test-project/locations/us-east1/jobs/test: HTTP Error: 400, Unknown Error", + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("jobFromEndpoint", () => { + const V1_ENDPOINT: backend.Endpoint = { + platform: "gcfv1", + id: "id", + region: "region", + project: "project", + entryPoint: "id", + runtime: "nodejs16", + scheduleTrigger: { + schedule: "every 1 minutes", + }, + }; + const V2_ENDPOINT: backend.Endpoint = { + ...V1_ENDPOINT, + platform: "gcfv2", + uri: "https://my-uri.com", + }; + + it("should copy minimal fields for v1 endpoints", () => { + expect( + cloudscheduler.jobFromEndpoint(V1_ENDPOINT, "appEngineLocation", "1234567"), + ).to.deep.equal({ + name: "projects/project/locations/appEngineLocation/jobs/firebase-schedule-id-region", + schedule: "every 1 minutes", + timeZone: "America/Los_Angeles", + pubsubTarget: { + topicName: "projects/project/topics/firebase-schedule-id-region", + attributes: { + scheduled: "true", + }, + }, + }); + }); + + it("should copy minimal fields for v2 endpoints", () => { + expect( + cloudscheduler.jobFromEndpoint(V2_ENDPOINT, V2_ENDPOINT.region, "1234567"), + ).to.deep.equal({ + name: "projects/project/locations/region/jobs/firebase-schedule-id-region", + schedule: "every 1 minutes", + timeZone: "UTC", + httpTarget: { + uri: "https://my-uri.com", + httpMethod: "POST", + oidcToken: { + serviceAccountEmail: "1234567-compute@developer.gserviceaccount.com", + }, + }, + }); + }); + + it("should copy optional fields for v1 endpoints", () => { + expect( + cloudscheduler.jobFromEndpoint( + { + ...V1_ENDPOINT, + scheduleTrigger: { + schedule: "every 1 minutes", + timeZone: "America/Los_Angeles", + retryConfig: { + maxDoublings: 2, + maxBackoffSeconds: 20, + minBackoffSeconds: 1, + maxRetrySeconds: 60, + }, + }, + }, + "appEngineLocation", + "1234567", + ), + ).to.deep.equal({ + name: "projects/project/locations/appEngineLocation/jobs/firebase-schedule-id-region", + schedule: "every 1 minutes", + timeZone: "America/Los_Angeles", + retryConfig: { + maxDoublings: 2, + maxBackoffDuration: "20s", + minBackoffDuration: "1s", + maxRetryDuration: "60s", + }, + pubsubTarget: { + topicName: "projects/project/topics/firebase-schedule-id-region", + attributes: { + scheduled: "true", + }, + }, + }); + }); + + it("should copy optional fields for v2 endpoints", () => { + expect( + cloudscheduler.jobFromEndpoint( + { + ...V2_ENDPOINT, + scheduleTrigger: { + schedule: "every 1 minutes", + timeZone: "America/Los_Angeles", + retryConfig: { + maxDoublings: 2, + maxBackoffSeconds: 20, + minBackoffSeconds: 1, + maxRetrySeconds: 60, + }, + }, + }, + V2_ENDPOINT.region, + "1234567", + ), + ).to.deep.equal({ + name: "projects/project/locations/region/jobs/firebase-schedule-id-region", + schedule: "every 1 minutes", + timeZone: "America/Los_Angeles", + retryConfig: { + maxDoublings: 2, + maxBackoffDuration: "20s", + minBackoffDuration: "1s", + maxRetryDuration: "60s", + }, + httpTarget: { + uri: "https://my-uri.com", + httpMethod: "POST", + oidcToken: { + serviceAccountEmail: "1234567-compute@developer.gserviceaccount.com", + }, + }, + }); + }); + }); +}); diff --git a/src/gcp/cloudscheduler.ts b/src/gcp/cloudscheduler.ts index 729c2c4aaa6..e82d96bdba2 100644 --- a/src/gcp/cloudscheduler.ts +++ b/src/gcp/cloudscheduler.ts @@ -2,13 +2,16 @@ import * as _ from "lodash"; import { FirebaseError } from "../error"; import { logger } from "../logger"; -import * as api from "../api"; +import { cloudschedulerOrigin } from "../api"; +import { Client } from "../apiv2"; import * as backend from "../deploy/functions/backend"; import * as proto from "./proto"; -import { assertExhaustive } from "../functional"; +import * as gce from "../gcp/computeEngine"; +import { assertExhaustive, nullsafeVisitor } from "../functional"; -const VERSION = "v1beta1"; -const DEFAULT_TIME_ZONE = "America/Los_Angeles"; +const VERSION = "v1"; +const DEFAULT_TIME_ZONE_V1 = "America/Los_Angeles"; +const DEFAULT_TIME_ZONE_V2 = "UTC"; export interface PubsubTarget { topicName: string; @@ -23,9 +26,9 @@ export interface OauthToken { scope: string; } -export interface OdicToken { +export interface OidcToken { serviceAccountEmail: string; - audiences: string[]; + audience?: string; } export interface HttpTarget { @@ -36,7 +39,7 @@ export interface HttpTarget { // oneof authorizationHeader oauthToken?: OauthToken; - odicToken?: OdicToken; + oidcToken?: OidcToken; // end oneof authorizationHeader; } @@ -51,7 +54,7 @@ export interface Job { name: string; schedule: string; description?: string; - timeZone?: string; + timeZone?: string | null; // oneof target httpTarget?: HttpTarget; @@ -59,42 +62,30 @@ export interface Job { // end oneof target retryConfig?: { - retryCount?: number; - maxRetryDuration?: string; - minBackoffDuration?: string; - maxBackoffDuration?: string; - maxDoublings?: number; + retryCount?: number | null; + maxRetryDuration?: string | null; + minBackoffDuration?: string | null; + maxBackoffDuration?: string | null; + maxDoublings?: number | null; }; } -export function assertValidJob(job: Job) { - proto.assertOneOf("Scheduler Job", job, "target", "httpTarget", "pubsubTarget"); - if (job.httpTarget) { - proto.assertOneOf( - "Scheduler Job", - job.httpTarget, - "httpTarget.authorizationHeader", - "oauthToken", - "odicToken" - ); - } -} +const apiClient = new Client({ urlPrefix: cloudschedulerOrigin(), apiVersion: VERSION }); /** * Creates a cloudScheduler job. * If another job with that name already exists, this will return a 409. * @param job The job to create. */ -export function createJob(job: Job): Promise { +function createJob(job: Job): Promise { // the replace below removes the portion of the schedule name after the last / // ie: projects/my-proj/locations/us-central1/jobs/firebase-schedule-func-us-east1 would become // projects/my-proj/locations/us-central1/jobs const strippedName = job.name.substring(0, job.name.lastIndexOf("/")); - return api.request("POST", `/${VERSION}/${strippedName}`, { - auth: true, - origin: api.cloudschedulerOrigin, - data: Object.assign({ timeZone: DEFAULT_TIME_ZONE }, job), - }); + const json: Job = job.pubsubTarget + ? { timeZone: DEFAULT_TIME_ZONE_V1, ...job } + : { timeZone: DEFAULT_TIME_ZONE_V2, ...job }; + return apiClient.post(`/${strippedName}`, json); } /** @@ -103,10 +94,7 @@ export function createJob(job: Job): Promise { * @param name The name of the job to delete. */ export function deleteJob(name: string): Promise { - return api.request("DELETE", `/${VERSION}/${name}`, { - auth: true, - origin: api.cloudschedulerOrigin, - }); + return apiClient.delete(`/${name}`); } /** @@ -115,9 +103,7 @@ export function deleteJob(name: string): Promise { * @param name The name of the job to get. */ export function getJob(name: string): Promise { - return api.request("GET", `/${VERSION}/${name}`, { - auth: true, - origin: api.cloudschedulerOrigin, + return apiClient.get(`/${name}`, { resolveOnHTTPError: true, }); } @@ -127,12 +113,23 @@ export function getJob(name: string): Promise { * Returns a 404 if no job with that name exists. * @param job A job to update. */ -export function updateJob(job: Job): Promise { - // Note that name cannot be updated. - return api.request("PATCH", `/${VERSION}/${job.name}`, { - auth: true, - origin: api.cloudschedulerOrigin, - data: Object.assign({ timeZone: DEFAULT_TIME_ZONE }, job), +function updateJob(job: Job): Promise { + let fieldMasks: string[]; + let json: Job; + if (job.pubsubTarget) { + // v1 uses pubsub + fieldMasks = proto.fieldMasks(job, "pubsubTarget"); + json = { timeZone: DEFAULT_TIME_ZONE_V1, ...job }; + } else { + // v2 uses http + fieldMasks = proto.fieldMasks(job, "httpTarget"); + json = { timeZone: DEFAULT_TIME_ZONE_V2, ...job }; + } + + return apiClient.patch(`/${job.name}`, json, { + queryParams: { + updateMask: fieldMasks.join(","), + }, }); } @@ -154,12 +151,12 @@ export async function createOrReplaceJob(job: Job): Promise { let newJob; try { newJob = await createJob(job); - } catch (err) { + } catch (err: any) { // Cloud resource location is not set so we error here and exit. if (err?.context?.response?.statusCode === 404) { throw new FirebaseError( `Cloud resource location is not set for this project but scheduled functions require it. ` + - `Please see this documentation for more details: https://firebase.google.com/docs/projects/locations.` + `Please see this documentation for more details: https://firebase.google.com/docs/projects/locations.`, ); } throw new FirebaseError(`Failed to create scheduler job ${job.name}: ${err.message}`); @@ -169,9 +166,9 @@ export async function createOrReplaceJob(job: Job): Promise { } if (!job.timeZone) { // We set this here to avoid recreating schedules that use the default timeZone - job.timeZone = DEFAULT_TIME_ZONE; + job.timeZone = job.pubsubTarget ? DEFAULT_TIME_ZONE_V1 : DEFAULT_TIME_ZONE_V2; } - if (isIdentical(existingJob.body, job)) { + if (!needUpdate(existingJob.body, job)) { logger.debug(`scheduler job ${jobName} is up to date, no changes required`); return; } @@ -182,47 +179,118 @@ export async function createOrReplaceJob(job: Job): Promise { /** * Check if two jobs are functionally equivalent. - * @param job a job to compare. - * @param otherJob a job to compare. + * @param existingJob a job to compare. + * @param newJob a job to compare. */ -function isIdentical(job: Job, otherJob: Job): boolean { - return ( - job && - otherJob && - job.schedule === otherJob.schedule && - job.timeZone === otherJob.timeZone && - _.isEqual(job.retryConfig, otherJob.retryConfig) - ); +function needUpdate(existingJob: Job, newJob: Job): boolean { + if (!existingJob) { + return true; + } + if (!newJob) { + return true; + } + if (existingJob.schedule !== newJob.schedule) { + return true; + } + if (existingJob.timeZone !== newJob.timeZone) { + return true; + } + if (newJob.retryConfig) { + if (!existingJob.retryConfig) { + return true; + } + if (!_.isMatch(existingJob.retryConfig, newJob.retryConfig)) { + return true; + } + } + return false; +} + +/** The name of the Cloud Scheduler job we will use for this endpoint. */ +export function jobNameForEndpoint( + endpoint: backend.Endpoint & backend.ScheduleTriggered, + location: string, +): string { + const id = backend.scheduleIdForFunction(endpoint); + return `projects/${endpoint.project}/locations/${location}/jobs/${id}`; +} + +/** The name of the pubsub topic that the Cloud Scheduler job will use for this endpoint. */ +export function topicNameForEndpoint( + endpoint: backend.Endpoint & backend.ScheduleTriggered, +): string { + const id = backend.scheduleIdForFunction(endpoint); + return `projects/${endpoint.project}/topics/${id}`; } /** Converts an Endpoint to a CloudScheduler v1 job */ export function jobFromEndpoint( endpoint: backend.Endpoint & backend.ScheduleTriggered, - appEngineLocation: string + location: string, + projectNumber: string, ): Job { const job: Partial = {}; + job.name = jobNameForEndpoint(endpoint, location); if (endpoint.platform === "gcfv1") { - const id = backend.scheduleIdForFunction(endpoint); - const region = appEngineLocation; - job.name = `projects/${endpoint.project}/locations/${region}/jobs/${id}`; + job.timeZone = endpoint.scheduleTrigger.timeZone || DEFAULT_TIME_ZONE_V1; job.pubsubTarget = { - topicName: `projects/${endpoint.project}/topics/${id}`, + topicName: topicNameForEndpoint(endpoint), attributes: { scheduled: "true", }, }; } else if (endpoint.platform === "gcfv2") { - // NB: We should figure out whether there's a good service account we can use - // to get ODIC tokens from while invoking the function. Hopefully either - // CloudScheduler has an account we can use or we can use the default compute - // account credentials (it's a project editor, so it should have permissions - // to invoke a function and editor deployers should have permission to actAs - // it) - throw new FirebaseError("Do not know how to create a scheduled GCFv2 function"); + job.timeZone = endpoint.scheduleTrigger.timeZone || DEFAULT_TIME_ZONE_V2; + job.httpTarget = { + uri: endpoint.uri!, + httpMethod: "POST", + oidcToken: { + serviceAccountEmail: endpoint.serviceAccount ?? gce.getDefaultServiceAccount(projectNumber), + }, + }; } else { assertExhaustive(endpoint.platform); } - proto.copyIfPresent(job, endpoint.scheduleTrigger, "schedule", "retryConfig", "timeZone"); + if (!endpoint.scheduleTrigger.schedule) { + throw new FirebaseError( + "Cannot create a scheduler job without a schedule:" + JSON.stringify(endpoint), + ); + } + job.schedule = endpoint.scheduleTrigger.schedule; + if (endpoint.scheduleTrigger.retryConfig) { + job.retryConfig = {}; + proto.copyIfPresent( + job.retryConfig, + endpoint.scheduleTrigger.retryConfig, + "maxDoublings", + "retryCount", + ); + proto.convertIfPresent( + job.retryConfig, + endpoint.scheduleTrigger.retryConfig, + "maxBackoffDuration", + "maxBackoffSeconds", + nullsafeVisitor(proto.durationFromSeconds), + ); + proto.convertIfPresent( + job.retryConfig, + endpoint.scheduleTrigger.retryConfig, + "minBackoffDuration", + "minBackoffSeconds", + nullsafeVisitor(proto.durationFromSeconds), + ); + proto.convertIfPresent( + job.retryConfig, + endpoint.scheduleTrigger.retryConfig, + "maxRetryDuration", + "maxRetrySeconds", + nullsafeVisitor(proto.durationFromSeconds), + ); + // If no retry configuration exists, delete the key to preserve existing retry config. + if (!Object.keys(job.retryConfig).length) { + delete job.retryConfig; + } + } // TypeScript compiler isn't noticing that name is defined in all code paths. return job as Job; diff --git a/src/gcp/cloudsql/cloudsqladmin.ts b/src/gcp/cloudsql/cloudsqladmin.ts new file mode 100755 index 00000000000..9ea454e9623 --- /dev/null +++ b/src/gcp/cloudsql/cloudsqladmin.ts @@ -0,0 +1,253 @@ +import { Client } from "../../apiv2"; +import { cloudSQLAdminOrigin } from "../../api"; +import * as operationPoller from "../../operation-poller"; +import { Instance, Database, User, UserType, DatabaseFlag } from "./types"; +import { FirebaseError } from "../../error"; +const API_VERSION = "v1"; + +const client = new Client({ + urlPrefix: cloudSQLAdminOrigin(), + auth: true, + apiVersion: API_VERSION, +}); + +interface Operation { + status: "RUNNING" | "DONE"; + name: string; +} + +export async function listInstances(projectId: string): Promise { + const res = await client.get<{ items: Instance[] }>(`projects/${projectId}/instances`); + return res.body.items ?? []; +} + +export async function getInstance(projectId: string, instanceId: string): Promise { + const res = await client.get(`projects/${projectId}/instances/${instanceId}`); + if (res.body.state === "FAILED") { + throw new FirebaseError( + `Cloud SQL instance ${instanceId} is in a failed state.\nGo to ${instanceConsoleLink(projectId, instanceId)} to repair or delete it.`, + ); + } + return res.body; +} + +/** Returns a link to Cloud SQL's page in Cloud Console. */ +export function instanceConsoleLink(projectId: string, instanceId: string) { + return `https://console.cloud.google.com/sql/instances/${instanceId}/overview?project=${projectId}`; +} + +export async function createInstance( + projectId: string, + location: string, + instanceId: string, + enableGoogleMlIntegration: boolean, + waitForCreation: boolean, +): Promise { + const databaseFlags = [{ name: "cloudsql.iam_authentication", value: "on" }]; + if (enableGoogleMlIntegration) { + databaseFlags.push({ name: "cloudsql.enable_google_ml_integration", value: "on" }); + } + const op = await client.post, Operation>(`projects/${projectId}/instances`, { + name: instanceId, + region: location, + databaseVersion: "POSTGRES_15", + settings: { + tier: "db-f1-micro", + edition: "ENTERPRISE", + ipConfiguration: { + authorizedNetworks: [], + }, + enableGoogleMlIntegration, + databaseFlags, + storageAutoResize: false, + userLabels: { "firebase-data-connect": "ft" }, + insightsConfig: { + queryInsightsEnabled: true, + queryPlansPerMinute: 5, // Match the default settings + queryStringLength: 1024, // Match the default settings + }, + }, + }); + if (!waitForCreation) { + return; + } + const opName = `projects/${projectId}/operations/${op.body.name}`; + const pollRes = await operationPoller.pollOperation({ + apiOrigin: cloudSQLAdminOrigin(), + apiVersion: API_VERSION, + operationResourceName: opName, + doneFn: (op: Operation) => op.status === "DONE", + masterTimeout: 1_200_000, // This operation frequently takes 5+ minutes + }); + return pollRes; +} + +/** + * Update an existing CloudSQL instance to have any required settings for Firebase Data Connect. + */ +export async function updateInstanceForDataConnect( + instance: Instance, + enableGoogleMlIntegration: boolean, +): Promise { + let dbFlags = setDatabaseFlag( + { name: "cloudsql.iam_authentication", value: "on" }, + instance.settings.databaseFlags, + ); + if (enableGoogleMlIntegration) { + dbFlags = setDatabaseFlag( + { name: "cloudsql.enable_google_ml_integration", value: "on" }, + dbFlags, + ); + } + + const op = await client.patch, Operation>( + `projects/${instance.project}/instances/${instance.name}`, + { + settings: { + ipConfiguration: { + ipv4Enabled: true, + }, + databaseFlags: dbFlags, + enableGoogleMlIntegration, + }, + }, + ); + const opName = `projects/${instance.project}/operations/${op.body.name}`; + const pollRes = await operationPoller.pollOperation({ + apiOrigin: cloudSQLAdminOrigin(), + apiVersion: API_VERSION, + operationResourceName: opName, + doneFn: (op: Operation) => op.status === "DONE", + masterTimeout: 1_200_000, // This operation frequently takes 5+ minutes + }); + return pollRes; +} + +function setDatabaseFlag(flag: DatabaseFlag, flags: DatabaseFlag[] = []): DatabaseFlag[] { + const temp = flags.filter((f) => f.name !== flag.name); + temp.push(flag); + return temp; +} + +export async function listDatabases(projectId: string, instanceId: string): Promise { + const res = await client.get<{ items: Database[] }>( + `projects/${projectId}/instances/${instanceId}/databases`, + ); + return res.body.items; +} + +export async function getDatabase( + projectId: string, + instanceId: string, + databaseId: string, +): Promise { + const res = await client.get( + `projects/${projectId}/instances/${instanceId}/databases/${databaseId}`, + ); + return res.body; +} + +export async function createDatabase( + projectId: string, + instanceId: string, + databaseId: string, +): Promise { + const op = await client.post<{ project: string; instance: string; name: string }, Operation>( + `projects/${projectId}/instances/${instanceId}/databases`, + { + project: projectId, + instance: instanceId, + name: databaseId, + }, + ); + + const opName = `projects/${projectId}/operations/${op.body.name}`; + const pollRes = await operationPoller.pollOperation({ + apiOrigin: cloudSQLAdminOrigin(), + apiVersion: API_VERSION, + operationResourceName: opName, + doneFn: (op: Operation) => op.status === "DONE", + }); + + return pollRes; +} + +export async function createUser( + projectId: string, + instanceId: string, + type: UserType, + username: string, + password?: string, +): Promise { + const maxRetries = 3; + let retries = 0; + while (true) { + try { + const op = await client.post( + `projects/${projectId}/instances/${instanceId}/users`, + { + name: username, + instance: instanceId, + project: projectId, + password: password, + sqlserverUserDetails: { + disabled: false, + serverRoles: ["cloudsqlsuperuser"], + }, + type, + }, + ); + const opName = `projects/${projectId}/operations/${op.body.name}`; + const pollRes = await operationPoller.pollOperation({ + apiOrigin: cloudSQLAdminOrigin(), + apiVersion: API_VERSION, + operationResourceName: opName, + doneFn: (op: Operation) => op.status === "DONE", + }); + return pollRes; + } catch (err: any) { + if (builtinRoleNotReady(err.message) && retries < maxRetries) { + retries++; + await new Promise((resolve) => { + setTimeout(resolve, 1000 * retries); + }); + } else { + throw err; + } + } + } +} + +// CloudSQL built in roles get created _after_ the operation is complete. +// This means that we occasionally bump into cases where we try to create the user +// before the role required for IAM users exists. +function builtinRoleNotReady(message: string): boolean { + return message.includes("cloudsqliamuser"); +} + +export async function getUser( + projectId: string, + instanceId: string, + username: string, +): Promise { + const res = await client.get( + `projects/${projectId}/instances/${instanceId}/users/${username}`, + ); + return res.body; +} + +export async function deleteUser(projectId: string, instanceId: string, username: string) { + const res = await client.delete(`projects/${projectId}/instances/${instanceId}/users`, { + queryParams: { + name: username, + }, + }); + return res.body; +} + +export async function listUsers(projectId: string, instanceId: string): Promise { + const res = await client.get<{ items: User[] }>( + `projects/${projectId}/instances/${instanceId}/users`, + ); + return res.body.items; +} diff --git a/src/gcp/cloudsql/connect.ts b/src/gcp/cloudsql/connect.ts new file mode 100644 index 00000000000..ad9a14834fc --- /dev/null +++ b/src/gcp/cloudsql/connect.ts @@ -0,0 +1,191 @@ +import * as pg from "pg"; +import { Connector, IpAddressTypes, AuthTypes } from "@google-cloud/cloud-sql-connector"; + +import { requireAuth } from "../../requireAuth"; +import { needProjectId } from "../../projectUtils"; +import * as cloudSqlAdminClient from "./cloudsqladmin"; +import { UserType } from "./types"; +import * as utils from "../../utils"; +import { logger } from "../../logger"; +import { FirebaseError } from "../../error"; +import { Options } from "../../options"; +import { FBToolsAuthClient } from "./fbToolsAuthClient"; + +export async function execute( + sqlStatements: string[], + opts: { + projectId: string; + instanceId: string; + databaseId: string; + username: string; + password?: string; + silent?: boolean; + }, +) { + const logFn = opts.silent ? logger.debug : logger.info; + const instance = await cloudSqlAdminClient.getInstance(opts.projectId, opts.instanceId); + const user = await cloudSqlAdminClient.getUser(opts.projectId, opts.instanceId, opts.username); + const connectionName = instance.connectionName; + if (!connectionName) { + throw new FirebaseError( + `Could not get instance connection string for ${opts.instanceId}:${opts.databaseId}`, + ); + } + let connector: Connector; + let pool: pg.Pool; + switch (user.type) { + case "CLOUD_IAM_USER": { + connector = new Connector({ + auth: new FBToolsAuthClient(), + }); + const clientOpts = await connector.getOptions({ + instanceConnectionName: connectionName, + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.IAM, + }); + pool = new pg.Pool({ + ...clientOpts, + user: opts.username, + database: opts.databaseId, + }); + break; + } + case "CLOUD_IAM_SERVICE_ACCOUNT": { + connector = new Connector(); + // Currently, this only works with Application Default credentials + // https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/issues/61 is an open + // FR to add support for OAuth2 tokens. + const clientOpts = await connector.getOptions({ + instanceConnectionName: connectionName, + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.IAM, + }); + pool = new pg.Pool({ + ...clientOpts, + user: opts.username, + database: opts.databaseId, + }); + break; + } + default: { + // Cloud SQL doesn't return user.type for BUILT_IN users... + if (!opts.password) { + throw new FirebaseError(`Cannot connect as BUILT_IN user without a password.`); + } + connector = new Connector({ + auth: new FBToolsAuthClient(), + }); + const clientOpts = await connector.getOptions({ + instanceConnectionName: connectionName, + ipType: IpAddressTypes.PUBLIC, + }); + pool = new pg.Pool({ + ...clientOpts, + user: opts.username, + password: opts.password, + database: opts.databaseId, + }); + break; + } + } + + const conn = await pool.connect(); + logFn(`Logged in as ${opts.username}`); + for (const s of sqlStatements) { + logFn(`Executing: '${s}'`); + try { + await conn.query(s); + } catch (err) { + throw new FirebaseError(`Error executing ${err}`); + } + } + + conn.release(); + await pool.end(); + connector.close(); +} + +// setupIAMUser sets up the current user identity to connect to CloudSQL. +// Steps: +// 2. Create an IAM user for the current identity +// 3. Connect to the DB as the temporary user and run the necessary grants +// 4. Deletes the temporary user +export async function setupIAMUser( + instanceId: string, + databaseId: string, + options: Options, +): Promise { + // TODO: Is there a good way to short circuit this by checking if the IAM user exists and has the appropriate role first? + const projectId = needProjectId(options); + // 0. Get the current identity + const account = await requireAuth(options); + if (!account) { + throw new FirebaseError( + "No account to set up! Run `firebase login` or set Application Default Credentials", + ); + } + // 1. Create a temporary builtin user + const setupUser = "firebasesuperuser"; + const temporaryPassword = utils.generateId(20); + await cloudSqlAdminClient.createUser( + projectId, + instanceId, + "BUILT_IN", + setupUser, + temporaryPassword, + ); + + // 2. Create an IAM user for the current identity + const { user, mode } = toDatabaseUser(account); + await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user); + + // 3. Connect to the DB as the temporary user and run the necessary grants + // TODO: I think we're missing something here, sometimes backend can't see the tables. + const grants = [ + `do + $$ + begin + if not exists (select FROM pg_catalog.pg_roles + WHERE rolname = '${firebaseowner(databaseId)}') then + CREATE ROLE "${firebaseowner(databaseId)}" WITH ADMIN "${setupUser}"; + end if; + end + $$ + ;`, + `GRANT ALL PRIVILEGES ON DATABASE "${databaseId}" TO "${firebaseowner(databaseId)}"`, + `GRANT cloudsqlsuperuser TO "${firebaseowner(databaseId)}"`, + `GRANT "${firebaseowner(databaseId)}" TO "${setupUser}"`, + `GRANT "${firebaseowner(databaseId)}" TO "${user}"`, + `ALTER SCHEMA public OWNER TO "${firebaseowner(databaseId)}"`, + `GRANT USAGE ON SCHEMA "public" TO PUBLIC`, + `GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA "public" TO PUBLIC`, + `GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA "public" TO PUBLIC`, + ]; + await execute(grants, { + projectId, + instanceId, + databaseId, + username: setupUser, + password: temporaryPassword, + silent: true, + }); + return user; +} + +export function firebaseowner(databaseId: string) { + return `firebaseowner_${databaseId}_public`; +} + +// Converts a account name to the equivalent SQL user. +// - Postgres: https://cloud.google.com/sql/docs/postgres/iam-logins#log-in-with-automatic +// - For user: it's full email address. +// - For service account: it's email address without the .gserviceaccount.com domain suffix. +function toDatabaseUser(account: string): { user: string; mode: UserType } { + let mode: UserType = "CLOUD_IAM_USER"; + let user = account; + if (account.endsWith(".gserviceaccount.com")) { + user = account.replace(".gserviceaccount.com", ""); + mode = "CLOUD_IAM_SERVICE_ACCOUNT"; + } + return { user, mode }; +} diff --git a/src/gcp/cloudsql/fbToolsAuthClient.ts b/src/gcp/cloudsql/fbToolsAuthClient.ts new file mode 100644 index 00000000000..53791303fca --- /dev/null +++ b/src/gcp/cloudsql/fbToolsAuthClient.ts @@ -0,0 +1,46 @@ +import { AuthClient } from "google-auth-library"; +import { GaxiosOptions, GaxiosPromise, GaxiosResponse } from "gaxios"; + +import * as apiv2 from "../../apiv2"; +import { FirebaseError } from "../../error"; + +// FBToolsAuthClient implements google-auth-library.AuthClient +// using apiv2.ts and our normal OAuth2 flow. +export class FBToolsAuthClient extends AuthClient { + public async request(opts: GaxiosOptions): GaxiosPromise { + if (!opts.url) { + throw new FirebaseError("opts.url was undefined"); + } + const url = new URL(opts.url as string); + const client = new apiv2.Client({ + urlPrefix: url.origin, + auth: true, + }); + const res = await client.request({ + method: opts.method ?? "POST", + path: url.pathname, + queryParams: opts.params, + body: opts.data, + responseType: opts.responseType, + }); + return { + config: opts, + status: res.status, + statusText: res.response.statusText, + data: res.body, + headers: res.response.headers, + request: {} as any, + }; + } + public async getAccessToken(): Promise<{ token?: string; res?: GaxiosResponse }> { + return { token: await apiv2.getAccessToken() }; + } + + public async getRequestHeaders(): Promise> { + const token = await this.getAccessToken(); + return { + ...apiv2.STANDARD_HEADERS, + Authorization: `Bearer ${token.token}`, + }; + } +} diff --git a/src/gcp/cloudsql/types.ts b/src/gcp/cloudsql/types.ts new file mode 100644 index 00000000000..76cee9748cc --- /dev/null +++ b/src/gcp/cloudsql/types.ts @@ -0,0 +1,120 @@ +export interface Database { + etag?: string; + name: string; + instance: string; + project: string; +} + +export interface IpConfiguration { + ipv4Enabled?: boolean; + privateNetwork?: string; + requireSsl?: boolean; + authorizedNetworks?: { + value: string; + expirationTime?: string; + name?: string; + }[]; + allocatedIpRange?: string; + sslMode?: + | "ALLOW_UNENCRYPTED_AND_ENCRYPTED" + | "ENCRYPTED_ONLY" + | "TRUSTED_CLIENT_CERTIFICATE_REQUIRED"; + pscConfig?: { + allowedConsumerProjects: string[]; + pscEnabled: boolean; + }; +} + +export interface InstanceSettings { + authorizedGaeApplications?: string[]; + tier?: string; + edition?: "ENTERPRISE_PLUS" | "ENTERPRISE"; + availabilityType?: "ZONAL" | "REGIONAL"; + pricingPlan?: "PER_USE" | "PACKAGE"; + replicationType?: "SYNCHRONOUS" | "ASYNCHRONOUS"; + activationPolicy?: "ALWAYS" | "NEVER"; + ipConfiguration?: IpConfiguration; + locationPreference?: [Object]; + databaseFlags?: DatabaseFlag[]; + dataDiskType?: "PD_SSD" | "PD_HDD"; + storageAutoResizeLimit?: string; + storageAutoResize?: boolean; + dataDiskSizeGb?: string; + deletionProtectionEnabled?: boolean; + dataCacheConfig?: { + dataCacheEnabled: boolean; + }; + enableGoogleMlIntegration?: boolean; + insightsConfig?: InsightsConfig; + userLabels?: { [key: string]: string }; +} + +export interface DatabaseFlag { + name: string; + value: string; +} + +interface InsightsConfig { + queryInsightsEnabled: boolean; + queryPlansPerMinute: number; + queryStringLength: number; +} + +// TODO: Consider splitting off return only fields and input fields into different types. +export interface Instance { + state?: "RUNNABLE" | "SUSPENDED" | "PENDING_DELETE" | "PENDING_CREATE" | "MAINTENANCE" | "FAILED"; + databaseVersion: + | "POSTGRES_15" + | "POSTGRES_14" + | "POSTGRES_13" + | "POSTGRES_12" + | "POSTGRES_11" + | string; + settings: InstanceSettings; + etag?: string; + rootPassword: string; + ipAddresses: { + type: "PRIMARY" | "OUTGOING" | "PRIVATE"; + ipAddress: string; + timeToRetire?: string; + }[]; + serverCaCert?: SslCert; + instanceType: "CLOUD_SQL_INSTANCE" | "ON_PREMISES_INSTANCE" | "READ_REPLICA_INSTANCE"; + project: string; + serviceAccountEmailAddress: string; + backendType: "SECOND_GEN" | "EXTERNAL"; + selfLink?: string; + connectionName?: string; + name: string; + region: string; + gceZone?: string; + databaseInstalledVersion?: string; + maintenanceVersion?: string; + createTime?: string; + sqlNetworkArchitecture?: string; +} + +export interface SslCert { + certSerialNumber: string; + cert: string; + commonName: string; + sha1Fingerprint: string; + instance: string; + createTime?: string; + expirationTime?: string; +} + +export interface User { + password?: string; + name: string; + host?: string; + instance: string; + project: string; + type: UserType; + sqlserverUserDetails: { + disabled: boolean; + serverRoles: string[]; + }; +} + +export type UserType = "BUILT_IN" | "CLOUD_IAM_USER" | "CLOUD_IAM_SERVICE_ACCOUNT"; diff --git a/src/test/gcp/cloudtasks.spec.ts b/src/gcp/cloudtasks.spec.ts similarity index 75% rename from src/test/gcp/cloudtasks.spec.ts rename to src/gcp/cloudtasks.spec.ts index 81cf4a8fd33..70c817d7d0c 100644 --- a/src/test/gcp/cloudtasks.spec.ts +++ b/src/gcp/cloudtasks.spec.ts @@ -1,9 +1,10 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as iam from "../../gcp/iam"; -import * as backend from "../../deploy/functions/backend"; -import * as cloudtasks from "../../gcp/cloudtasks"; +import * as iam from "./iam"; +import * as backend from "../deploy/functions/backend"; +import * as cloudtasks from "./cloudtasks"; +import * as proto from "./proto"; describe("CloudTasks", () => { let ct: sinon.SinonStubbedInstance; @@ -21,6 +22,7 @@ describe("CloudTasks", () => { ct = sinon.stub(cloudtasks); ct.queueNameForEndpoint.restore(); ct.queueFromEndpoint.restore(); + ct.triggerFromQueue.restore(); ct.setEnqueuer.restore(); ct.upsertQueue.restore(); }); @@ -39,16 +41,15 @@ describe("CloudTasks", () => { it("handles complex endpoints", () => { const rateLimits: backend.TaskQueueRateLimits = { - maxBurstSize: 100, maxConcurrentDispatches: 5, maxDispatchesPerSecond: 5, }; const retryConfig: backend.TaskQueueRetryConfig = { maxAttempts: 10, - maxBackoff: "60s", maxDoublings: 9, - maxRetryDuration: "300s", - minBackoff: "1s", + maxBackoffSeconds: 60, + maxRetrySeconds: 300, + minBackoffSeconds: 1, }; const ep: backend.Endpoint = { @@ -62,12 +63,66 @@ describe("CloudTasks", () => { expect(cloudtasks.queueFromEndpoint(ep)).to.deep.equal({ name: "projects/project/locations/region/queues/id", rateLimits, - retryConfig, + retryConfig: { + maxAttempts: 10, + maxDoublings: 9, + maxRetryDuration: "300s", + maxBackoff: "60s", + minBackoff: "1s", + }, state: "RUNNING", }); }); }); + describe("triggerFromQueue", () => { + it("handles queue with default settings", () => { + expect( + cloudtasks.triggerFromQueue({ + name: "projects/project/locations/region/queues/id", + ...cloudtasks.DEFAULT_SETTINGS, + }), + ).to.deep.equal({ + rateLimits: { ...cloudtasks.DEFAULT_SETTINGS.rateLimits }, + retryConfig: { + maxAttempts: cloudtasks.DEFAULT_SETTINGS.retryConfig?.maxAttempts, + maxDoublings: cloudtasks.DEFAULT_SETTINGS.retryConfig?.maxDoublings, + maxBackoffSeconds: proto.secondsFromDuration( + cloudtasks.DEFAULT_SETTINGS.retryConfig?.maxBackoff || "", + ), + minBackoffSeconds: proto.secondsFromDuration( + cloudtasks.DEFAULT_SETTINGS.retryConfig?.minBackoff || "", + ), + }, + }); + }); + + it("handles queue with custom configs", () => { + expect( + cloudtasks.triggerFromQueue({ + name: "projects/project/locations/region/queues/id", + rateLimits: { + maxConcurrentDispatches: 5, + maxDispatchesPerSecond: 5, + }, + retryConfig: { + maxAttempts: 10, + maxDoublings: 9, + }, + }), + ).to.deep.equal({ + rateLimits: { + maxConcurrentDispatches: 5, + maxDispatchesPerSecond: 5, + }, + retryConfig: { + maxAttempts: 10, + maxDoublings: 9, + }, + }); + }); + }); + describe("upsertEndpoint", () => { it("accepts a matching queue", async () => { const queue: cloudtasks.Queue = { @@ -88,7 +143,7 @@ describe("CloudTasks", () => { name: "projects/p/locations/r/queues/f", ...cloudtasks.DEFAULT_SETTINGS, rateLimits: { - maxBurstSize: 9_000, + maxConcurrentDispatches: 20, }, }; const haveQueue: cloudtasks.Queue = { diff --git a/src/gcp/cloudtasks.ts b/src/gcp/cloudtasks.ts index 6e48ab2587a..f3c10decea6 100644 --- a/src/gcp/cloudtasks.ts +++ b/src/gcp/cloudtasks.ts @@ -4,11 +4,12 @@ import { Client } from "../apiv2"; import { cloudTasksOrigin } from "../api"; import * as iam from "./iam"; import * as backend from "../deploy/functions/backend"; +import { nullsafeVisitor } from "../functional"; const API_VERSION = "v2"; const client = new Client({ - urlPrefix: cloudTasksOrigin, + urlPrefix: cloudTasksOrigin(), auth: true, apiVersion: API_VERSION, }); @@ -21,17 +22,16 @@ export interface AppEngineRouting { } export interface RateLimits { - maxDispatchesPerSecond?: number; - maxBurstSize?: number; - maxConcurrentDispatches?: number; + maxDispatchesPerSecond?: number | null; + maxConcurrentDispatches?: number | null; } export interface RetryConfig { - maxAttempts?: number; - maxRetryDuration?: proto.Duration; - minBackoff?: proto.Duration; - maxBackoff?: proto.Duration; - maxDoublings?: number; + maxAttempts?: number | null; + maxRetryDuration?: proto.Duration | null; + minBackoff?: proto.Duration | null; + maxBackoff?: proto.Duration | null; + maxDoublings?: number | null; } export interface StackdriverLoggingConfig { @@ -69,7 +69,6 @@ export interface Queue { export const DEFAULT_SETTINGS: Omit = { rateLimits: { maxConcurrentDispatches: 1000, - maxBurstSize: 100, maxDispatchesPerSecond: 500, }, state: "RUNNING", @@ -117,7 +116,7 @@ export async function upsertQueue(queue: Queue): Promise { await (module.exports.updateQueue as typeof updateQueue)(queue); return false; - } catch (err) { + } catch (err: any) { if (err?.context?.response?.statusCode === 404) { await (module.exports.createQueue as typeof createQueue)(queue); return true; @@ -156,7 +155,7 @@ const ENQUEUER_ROLE = "roles/cloudtasks.enqueuer"; export async function setEnqueuer( name: string, invoker: string[], - assumeEmpty: boolean = false + assumeEmpty = false, ): Promise { let existing: iam.Policy; if (assumeEmpty) { @@ -173,7 +172,7 @@ export async function setEnqueuer( const invokerMembers = proto.getInvokerMembers(invoker, project); while (true) { const policy: iam.Policy = { - bindings: existing.bindings.filter((binding) => binding.role != ENQUEUER_ROLE), + bindings: existing.bindings.filter((binding) => binding.role !== ENQUEUER_ROLE), etag: existing.etag, version: existing.version, }; @@ -189,7 +188,7 @@ export async function setEnqueuer( try { await (module.exports.setIamPolicy as typeof setIamPolicy)(name, policy); return; - } catch (err) { + } catch (err: any) { // Re-fetch on conflict if (err?.context?.response?.statusCode === 429) { existing = await (module.exports.getIamPolicy as typeof getIamPolicy)(name); @@ -202,7 +201,7 @@ export async function setEnqueuer( /** The name of the Task Queue we will use for this endpoint. */ export function queueNameForEndpoint( - endpoint: backend.Endpoint & backend.TaskQueueTriggered + endpoint: backend.Endpoint & backend.TaskQueueTriggered, ): string { return `projects/${endpoint.project}/locations/${endpoint.region}/queues/${endpoint.id}`; } @@ -217,9 +216,8 @@ export function queueFromEndpoint(endpoint: backend.Endpoint & backend.TaskQueue proto.copyIfPresent( queue.rateLimits, endpoint.taskQueueTrigger.rateLimits, - "maxBurstSize", "maxConcurrentDispatches", - "maxDispatchesPerSecond" + "maxDispatchesPerSecond", ); } if (endpoint.taskQueueTrigger.retryConfig) { @@ -227,11 +225,74 @@ export function queueFromEndpoint(endpoint: backend.Endpoint & backend.TaskQueue queue.retryConfig, endpoint.taskQueueTrigger.retryConfig, "maxAttempts", - "maxBackoff", "maxDoublings", + ); + proto.convertIfPresent( + queue.retryConfig, + endpoint.taskQueueTrigger.retryConfig, "maxRetryDuration", - "minBackoff" + "maxRetrySeconds", + nullsafeVisitor(proto.durationFromSeconds), + ); + proto.convertIfPresent( + queue.retryConfig, + endpoint.taskQueueTrigger.retryConfig, + "maxBackoff", + "maxBackoffSeconds", + nullsafeVisitor(proto.durationFromSeconds), + ); + proto.convertIfPresent( + queue.retryConfig, + endpoint.taskQueueTrigger.retryConfig, + "minBackoff", + "minBackoffSeconds", + nullsafeVisitor(proto.durationFromSeconds), ); } return queue; } + +/** Creates a trigger type from API type */ +export function triggerFromQueue(queue: Queue): backend.TaskQueueTriggered["taskQueueTrigger"] { + const taskQueueTrigger: backend.TaskQueueTriggered["taskQueueTrigger"] = {}; + if (queue.rateLimits) { + taskQueueTrigger.rateLimits = {}; + proto.copyIfPresent( + taskQueueTrigger.rateLimits, + queue.rateLimits, + "maxConcurrentDispatches", + "maxDispatchesPerSecond", + ); + } + if (queue.retryConfig) { + taskQueueTrigger.retryConfig = {}; + proto.copyIfPresent( + taskQueueTrigger.retryConfig, + queue.retryConfig, + "maxAttempts", + "maxDoublings", + ); + proto.convertIfPresent( + taskQueueTrigger.retryConfig, + queue.retryConfig, + "maxRetrySeconds", + "maxRetryDuration", + nullsafeVisitor(proto.secondsFromDuration), + ); + proto.convertIfPresent( + taskQueueTrigger.retryConfig, + queue.retryConfig, + "maxBackoffSeconds", + "maxBackoff", + nullsafeVisitor(proto.secondsFromDuration), + ); + proto.convertIfPresent( + taskQueueTrigger.retryConfig, + queue.retryConfig, + "minBackoffSeconds", + "minBackoff", + nullsafeVisitor(proto.secondsFromDuration), + ); + } + return taskQueueTrigger; +} diff --git a/src/gcp/computeEngine.ts b/src/gcp/computeEngine.ts new file mode 100644 index 00000000000..28a1746974c --- /dev/null +++ b/src/gcp/computeEngine.ts @@ -0,0 +1,4 @@ +/** Returns the default compute engine service agent */ +export function getDefaultServiceAccount(projectNumber: string): string { + return `${projectNumber}-compute@developer.gserviceaccount.com`; +} diff --git a/src/gcp/devConnect.ts b/src/gcp/devConnect.ts new file mode 100644 index 00000000000..a69d63cd3bc --- /dev/null +++ b/src/gcp/devConnect.ts @@ -0,0 +1,295 @@ +import { Client } from "../apiv2"; +import { developerConnectOrigin, developerConnectP4SADomain } from "../api"; +import { generateServiceIdentityAndPoll } from "./serviceusage"; + +const PAGE_SIZE_MAX = 1000; +const LOCATION_OVERRIDE = process.env.FIREBASE_DEVELOPERCONNECT_LOCATION_OVERRIDE; + +export const client = new Client({ + urlPrefix: developerConnectOrigin(), + auth: true, + apiVersion: "v1", +}); + +export interface OperationMetadata { + createTime: string; + endTime: string; + target: string; + verb: string; + requestedCancellation: boolean; + apiVersion: string; +} + +export interface Operation { + name: string; + metadata?: OperationMetadata; + done: boolean; + error?: { code: number; message: string; details: unknown }; + response?: any; +} + +export interface OAuthCredential { + oauthTokenSecretVersion: string; + username: string; +} + +type GitHubApp = "GIT_HUB_APP_UNSPECIFIED" | "DEVELOPER_CONNECT" | "FIREBASE"; + +export interface GitHubConfig { + githubApp?: GitHubApp; + authorizerCredential?: OAuthCredential; + appInstallationId?: string; + installationUri?: string; +} + +type InstallationStage = + | "STAGE_UNSPECIFIED" + | "PENDING_CREATE_APP" + | "PENDING_USER_OAUTH" + | "PENDING_INSTALL_APP" + | "COMPLETE"; + +export interface InstallationState { + stage: InstallationStage; + message: string; + actionUri: string; +} + +export interface Connection { + name: string; + createTime?: string; + updateTime?: string; + deleteTime?: string; + labels?: { + [key: string]: string; + }; + githubConfig?: GitHubConfig; + installationState: InstallationState; + disabled?: boolean; + reconciling?: boolean; + annotations?: { + [key: string]: string; + }; + etag?: string; + uid?: string; +} + +type ConnectionOutputOnlyFields = + | "createTime" + | "updateTime" + | "deleteTime" + | "installationState" + | "reconciling" + | "uid"; + +export interface GitRepositoryLink { + name: string; + cloneUri: string; + createTime: string; + updateTime: string; + deleteTime: string; + labels?: { + [key: string]: string; + }; + etag?: string; + reconciling: boolean; + annotations?: { + [key: string]: string; + }; + uid: string; +} + +type GitRepositoryLinkOutputOnlyFields = + | "createTime" + | "updateTime" + | "deleteTime" + | "reconciling" + | "uid"; + +export interface LinkableGitRepositories { + linkableGitRepositories: LinkableGitRepository[]; + nextPageToken: string; +} + +export interface LinkableGitRepository { + cloneUri: string; +} + +/** + * Creates a Developer Connect Connection. + */ +export async function createConnection( + projectId: string, + location: string, + connectionId: string, + githubConfig: GitHubConfig = {}, +): Promise { + const config: GitHubConfig = { + ...githubConfig, + githubApp: "FIREBASE", + }; + const res = await client.post< + Omit, ConnectionOutputOnlyFields>, + Operation + >( + `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections`, + { + githubConfig: config, + }, + { queryParams: { connectionId } }, + ); + return res.body; +} + +/** + * Deletes a connection that matches the given parameters + */ +export async function deleteConnection( + projectId: string, + location: string, + connectionId: string, +): Promise { + /** + * TODO: specify a unique request ID so that if you must retry your request, + * the server will know to ignore the request if it has already been + * completed. The server will guarantee that for at least 60 minutes after + * the first request. + */ + const name = `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections/${connectionId}`; + const res = await client.delete(name, { queryParams: { force: "true" } }); + return res.body; +} + +/** + * Gets details of a single Developer Connect Connection. + */ +export async function getConnection( + projectId: string, + location: string, + connectionId: string, +): Promise { + const name = `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections/${connectionId}`; + const res = await client.get(name); + return res.body; +} + +/** + * List Developer Connect Connections + */ +export async function listAllConnections( + projectId: string, + location: string, +): Promise { + const conns: Connection[] = []; + const getNextPage = async (pageToken = ""): Promise => { + const res = await client.get<{ + connections: Connection[]; + nextPageToken?: string; + }>(`/projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections`, { + queryParams: { + pageSize: PAGE_SIZE_MAX, + pageToken, + }, + }); + if (Array.isArray(res.body.connections)) { + conns.push(...res.body.connections); + } + if (res.body.nextPageToken) { + await getNextPage(res.body.nextPageToken); + } + }; + await getNextPage(); + return conns; +} + +/** + * Gets a list of repositories that can be added to the provided Connection. + */ +export async function listAllLinkableGitRepositories( + projectId: string, + location: string, + connectionId: string, +): Promise { + const name = `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections/${connectionId}:fetchLinkableGitRepositories`; + const repos: LinkableGitRepository[] = []; + + const getNextPage = async (pageToken = ""): Promise => { + const res = await client.get(name, { + queryParams: { + pageSize: PAGE_SIZE_MAX, + pageToken, + }, + }); + + if (Array.isArray(res.body.linkableGitRepositories)) { + repos.push(...res.body.linkableGitRepositories); + } + + if (res.body.nextPageToken) { + await getNextPage(res.body.nextPageToken); + } + }; + + await getNextPage(); + return repos; +} + +/** + * Creates a GitRepositoryLink.Upon linking a Git Repository, Developer + * Connect will configure the Git Repository to send webhook events to + * Developer Connect. + */ +export async function createGitRepositoryLink( + projectId: string, + location: string, + connectionId: string, + gitRepositoryLinkId: string, + cloneUri: string, +): Promise { + const res = await client.post< + Omit, + Operation + >( + `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections/${connectionId}/gitRepositoryLinks`, + { cloneUri }, + { queryParams: { gitRepositoryLinkId } }, + ); + return res.body; +} + +/** + * Get details of a single GitRepositoryLink + */ +export async function getGitRepositoryLink( + projectId: string, + location: string, + connectionId: string, + gitRepositoryLinkId: string, +): Promise { + const name = `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections/${connectionId}/gitRepositoryLinks/${gitRepositoryLinkId}`; + const res = await client.get(name); + return res.body; +} + +/** + * Returns email associated with the Developer Connect Service Agent + */ +export function serviceAgentEmail(projectNumber: string): string { + return `service-${projectNumber}@${developerConnectP4SADomain()}`; +} + +/** + * Generates the Developer Connect P4SA which is required to use the Developer + * Connect APIs. + * @param projectNumber the project number for which this P4SA is being + * generated for. + */ +export async function generateP4SA(projectNumber: string): Promise { + const devConnectOrigin = developerConnectOrigin(); + + await generateServiceIdentityAndPoll( + projectNumber, + new URL(devConnectOrigin).hostname, + "apphosting", + ); +} diff --git a/src/gcp/devconnect.spec.ts b/src/gcp/devconnect.spec.ts new file mode 100644 index 00000000000..355c3e0c936 --- /dev/null +++ b/src/gcp/devconnect.spec.ts @@ -0,0 +1,107 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as devconnect from "./devConnect"; + +describe("developer connect", () => { + let post: sinon.SinonStub; + let get: sinon.SinonStub; + + const projectId = "project"; + const location = "us-central1"; + const connectionId = "apphosting-connection"; + const connectionsRequestPath = `projects/${projectId}/locations/${location}/connections`; + + beforeEach(() => { + post = sinon.stub(devconnect.client, "post"); + get = sinon.stub(devconnect.client, "get"); + }); + + afterEach(() => { + post.restore(); + get.restore(); + }); + + describe("createConnection", () => { + it("ensures githubConfig is FIREBASE", async () => { + post.returns({ body: {} }); + await devconnect.createConnection(projectId, location, connectionId, {}); + + expect(post).to.be.calledWith( + connectionsRequestPath, + { githubConfig: { githubApp: "FIREBASE" } }, + { queryParams: { connectionId } }, + ); + }); + }); + + describe("listConnections", () => { + it("interates through all pages and returns a single list", async () => { + const firstConnection = { name: "conn1", installationState: { stage: "COMPLETE" } }; + const secondConnection = { name: "conn2", installationState: { stage: "COMPLETE" } }; + const thirdConnection = { name: "conn3", installationState: { stage: "COMPLETE" } }; + + get + .onFirstCall() + .returns({ + body: { + connections: [firstConnection], + nextPageToken: "someToken", + }, + }) + .onSecondCall() + .returns({ + body: { + connections: [secondConnection], + nextPageToken: "someToken2", + }, + }) + .onThirdCall() + .returns({ + body: { + connections: [thirdConnection], + }, + }); + + const conns = await devconnect.listAllConnections(projectId, location); + expect(get).callCount(3); + expect(conns).to.deep.equal([firstConnection, secondConnection, thirdConnection]); + }); + }); + describe("listAllLinkableGitRepositories", () => { + it("interates through all pages and returns a single list", async () => { + const firstRepo = { cloneUri: "repo1" }; + const secondRepo = { cloneUri: "repo2" }; + const thirdRepo = { cloneUri: "repo3" }; + + get + .onFirstCall() + .returns({ + body: { + linkableGitRepositories: [firstRepo], + nextPageToken: "someToken", + }, + }) + .onSecondCall() + .returns({ + body: { + linkableGitRepositories: [secondRepo], + nextPageToken: "someToken2", + }, + }) + .onThirdCall() + .returns({ + body: { + linkableGitRepositories: [thirdRepo], + }, + }); + + const conns = await devconnect.listAllLinkableGitRepositories( + projectId, + location, + connectionId, + ); + expect(get).callCount(3); + expect(conns).to.deep.equal([firstRepo, secondRepo, thirdRepo]); + }); + }); +}); diff --git a/src/gcp/docker.ts b/src/gcp/docker.ts index d0b324dd990..623f14c3e8d 100644 --- a/src/gcp/docker.ts +++ b/src/gcp/docker.ts @@ -104,7 +104,7 @@ export class Client { if (!response.body) { return; } - if (response.body.errors?.length != 0) { + if (response.body.errors?.length !== 0) { throw new FirebaseError(`Failed to delete tag ${tag} at path ${path}`, { children: response.body.errors, }); @@ -116,7 +116,7 @@ export class Client { if (!response.body) { return; } - if (response.body.errors?.length != 0) { + if (response.body.errors?.length !== 0) { throw new FirebaseError(`Failed to delete image ${digest} at path ${path}`, { children: response.body.errors, }); diff --git a/src/gcp/eventarc.ts b/src/gcp/eventarc.ts new file mode 100644 index 00000000000..038d08524cb --- /dev/null +++ b/src/gcp/eventarc.ts @@ -0,0 +1,94 @@ +import { Client } from "../apiv2"; +import { eventarcOrigin } from "../api"; +import { last } from "lodash"; +import { fieldMasks } from "./proto"; + +export const API_VERSION = "v1"; + +export interface Channel { + name: string; + + /** Server-assigned uinique identifier. Format is a UUID4 */ + uid?: string; + + createTime?: string; + updateTime?: string; + + /** If set, the channel will grant publish permissions to the 2P provider. */ + provider?: string; + + // BEGIN oneof transport + pubsubTopic?: string; + // END oneof transport + + state?: "PENDING" | "ACTIVE" | "INACTIVE"; + + /** When the channel is `PENDING`, this token must be sent to the provider */ + activationToken?: string; + + cryptoKeyName?: string; +} + +interface OperationMetadata { + createTime: string; + target: string; + verb: string; + requestedCancellation: boolean; + apiVersion: string; +} + +interface Operation { + name: string; + metadata: OperationMetadata; + done: boolean; +} + +const client = new Client({ + urlPrefix: eventarcOrigin(), + auth: true, + apiVersion: API_VERSION, +}); + +/** + * Gets a Channel. + */ +export async function getChannel(name: string): Promise { + const res = await client.get(name, { resolveOnHTTPError: true }); + if (res.status === 404) { + return undefined; + } + return res.body; +} + +/** + * Creates a channel. + */ +export async function createChannel(channel: Channel): Promise { + // const body: Partial = cloneDeep(channel); + const pathParts = channel.name.split("/"); + + const res = await client.post(pathParts.slice(0, -1).join("/"), channel, { + queryParams: { channelId: last(pathParts)! }, + }); + return res.body; +} + +/** + * Updates a channel to match the new spec. + * Only set fields are updated. + */ +export async function updateChannel(channel: Channel): Promise { + const res = await client.put(channel.name, channel, { + queryParams: { + updateMask: fieldMasks(channel).join(","), + }, + }); + return res.body; +} + +/** + * Deletes a channel. + */ +export async function deleteChannel(name: string): Promise { + await client.delete(name); +} diff --git a/src/gcp/firedata.spec.ts b/src/gcp/firedata.spec.ts new file mode 100644 index 00000000000..ab0f9bdac83 --- /dev/null +++ b/src/gcp/firedata.spec.ts @@ -0,0 +1,73 @@ +import * as nock from "nock"; +import { + APPHOSTING_TOS_ID, + APP_CHECK_TOS_ID, + GetTosStatusResponse, + getAcceptanceStatus, + getTosStatus, + isProductTosAccepted, +} from "./firedata"; +import { expect } from "chai"; + +const SAMPLE_RESPONSE = { + perServiceStatus: [ + { + tosId: "APP_CHECK", + serviceStatus: { + tos: { + id: "app_check", + tosId: "APP_CHECK", + }, + status: "ACCEPTED", + }, + }, + { + tosId: "APP_HOSTING_TOS", + serviceStatus: { + tos: { + id: "app_hosting", + tosId: "APP_HOSTING_TOS", + }, + status: "TERMS_UPDATED", + }, + }, + ], +}; + +describe("firedata", () => { + before(() => { + nock.disableNetConnect(); + }); + after(() => { + nock.cleanAll(); + nock.enableNetConnect(); + }); + + describe("getTosStatus", () => { + it("should return parsed GetTosStatusResponse", async () => { + nock("https://mobilesdk-pa.googleapis.com") + .get("/v1/accessmanagement/tos:getStatus") + .reply(200, SAMPLE_RESPONSE); + + await expect(getTosStatus()).to.eventually.deep.equal( + SAMPLE_RESPONSE as GetTosStatusResponse, + ); + }); + }); + + describe("getAcceptanceStatus", () => { + it("should return the status", () => { + const res = SAMPLE_RESPONSE as GetTosStatusResponse; + expect(getAcceptanceStatus(res, APP_CHECK_TOS_ID)).to.equal("ACCEPTED"); + expect(getAcceptanceStatus(res, APPHOSTING_TOS_ID)).to.equal("TERMS_UPDATED"); + }); + }); + + describe("isProductTosAccepted", () => { + it("should determine whether tos is accepted", () => { + const res = SAMPLE_RESPONSE as GetTosStatusResponse; + expect(isProductTosAccepted(res, APP_CHECK_TOS_ID)).to.equal(true); + expect(isProductTosAccepted(res, APPHOSTING_TOS_ID)).to.equal(false); + }); + }); +}); diff --git a/src/gcp/firedata.ts b/src/gcp/firedata.ts index 55ed6093c84..f395708c68c 100644 --- a/src/gcp/firedata.ts +++ b/src/gcp/firedata.ts @@ -1,36 +1,51 @@ -import * as api from "../api"; -import { logger } from "../logger"; -import * as utils from "../utils"; - -export interface DatabaseInstance { - // The globally unique name of the Database instance. - // Required to be URL safe. ex: 'red-ant' - instance: string; +import { Client } from "../apiv2"; +import { firedataOrigin } from "../api"; +import { FirebaseError } from "../error"; + +const client = new Client({ urlPrefix: firedataOrigin(), auth: true, apiVersion: "v1" }); + +export const APPHOSTING_TOS_ID = "APP_HOSTING_TOS"; +export const APP_CHECK_TOS_ID = "APP_CHECK"; +export const DATA_CONNECT_TOS_ID = "FIREBASE_DATA_CONNECT"; + +export type TosId = typeof APPHOSTING_TOS_ID | typeof APP_CHECK_TOS_ID | typeof DATA_CONNECT_TOS_ID; + +export type AcceptanceStatus = null | "ACCEPTED" | "TERMS_UPDATED"; + +export interface TosAcceptanceStatus { + status: AcceptanceStatus; } -function _handleErrorResponse(response: any): any { - if (response.body && response.body.error) { - return utils.reject(response.body.error, { code: 2 }); - } +export interface ServiceTosStatus { + tosId: TosId; + serviceStatus: TosAcceptanceStatus; +} - logger.debug("[firedata] error:", response.status, response.body); - return utils.reject("Unexpected error encountered with FireData.", { - code: 2, - }); +export interface GetTosStatusResponse { + perServiceStatus: ServiceTosStatus[]; } /** - * List Realtime Database instances - * @param projectNumber Project from which you want to list databases. - * @return the list of databases. + * Fetches the Terms of Service status for the logged in user. */ -export async function listDatabaseInstances(projectNumber: string): Promise { - const response = await api.request("GET", `/v1/projects/${projectNumber}/databases`, { - auth: true, - origin: api.firedataOrigin, - }); - if (response.status === 200) { - return response.body.instance; +export async function getTosStatus(): Promise { + const res = await client.get("accessmanagement/tos:getStatus"); + return res.body; +} + +/** Returns the AcceptanceStatus for a given product. */ +export function getAcceptanceStatus( + response: GetTosStatusResponse, + tosId: TosId, +): AcceptanceStatus { + const perServiceStatus = response.perServiceStatus.find((tosStatus) => tosStatus.tosId === tosId); + if (perServiceStatus === undefined) { + throw new FirebaseError(`Missing terms of service status for product: ${tosId}`); } - return _handleErrorResponse(response); + return perServiceStatus.serviceStatus.status; +} + +/** Returns true if a product's ToS has been accepted. */ +export function isProductTosAccepted(response: GetTosStatusResponse, tosId: TosId): boolean { + return getAcceptanceStatus(response, tosId) === "ACCEPTED"; } diff --git a/src/gcp/firestore.ts b/src/gcp/firestore.ts index ecbe59626b3..eb500f38e83 100644 --- a/src/gcp/firestore.ts +++ b/src/gcp/firestore.ts @@ -1,26 +1,119 @@ -import { firestoreOriginOrEmulator } from "../api"; -import * as apiv2 from "../apiv2"; +import { firestoreOrigin, firestoreOriginOrEmulator } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { Duration, assertOneOf, durationFromSeconds } from "./proto"; +import { FirebaseError } from "../error"; -const _CLIENT = new apiv2.Client({ +const prodOnlyClient = new Client({ auth: true, apiVersion: "v1", - urlPrefix: firestoreOriginOrEmulator, + urlPrefix: firestoreOrigin(), }); +const emuOrProdClient = new Client({ + auth: true, + apiVersion: "v1", + urlPrefix: firestoreOriginOrEmulator(), +}); + +export interface Database { + name: string; + uid: string; + createTime: string; + updateTime: string; + locationId: string; + type: "DATABASE_TYPE_UNSPECIFIED" | "FIRESTORE_NATIVE" | "DATASTORE_MODE"; + concurrencyMode: + | "CONCURRENCY_MODE_UNSPECIFIED" + | "OPTIMISTIC" + | "PESSIMISTIC" + | "OPTIMISTIC_WITH_ENTITY_GROUPS"; + appEngineIntegrationMode: "APP_ENGINE_INTEGRATION_MODE_UNSPECIFIED" | "ENABLED" | "DISABLED"; + keyPrefix: string; + etag: string; +} + +export enum DayOfWeek { + MONDAY = "MONDAY", + TUEDAY = "TUESDAY", + WEDNESDAY = "WEDNESDAY", + THURSDAY = "THURSDAY", + FRIDAY = "FRIDAY", + SATURDAY = "SATURDAY", + SUNDAY = "SUNDAY", +} +// No DailyRecurrence type as it would just be an empty interface +export interface WeeklyRecurrence { + day: DayOfWeek; +} + +export interface BackupSchedule { + name?: string; + createTime?: string; + updateTime?: string; + retention: Duration; + + // oneof recurrence + dailyRecurrence?: Record; // Typescript for "empty object" + weeklyRecurrence?: WeeklyRecurrence; + // end oneof recurrence +} + +export interface Backup { + name?: string; + database?: string; + databaseUid?: string; + snapshotTime?: string; + expireTime?: string; + stats?: string; + state?: "CREATING" | "READY" | "NOT_AVAILABLE"; +} + +export interface ListBackupsResponse { + backups?: Backup[]; + unreachable?: string[]; +} + +/** + * Get a firebase database instance. + * @param {string} project the Google Cloud project + * @param {string} database the Firestore database name + */ +export async function getDatabase( + project: string, + database: string, + allowEmulator: boolean = false, +): Promise { + const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient; + const url = `projects/${project}/databases/${database}`; + try { + const resp = await apiClient.get(url); + return resp.body; + } catch (err: unknown) { + logger.info( + `There was an error retrieving the Firestore database. Currently, the database id is set to ${database}, make sure it exists.`, + ); + throw err; + } +} + /** * List all collection IDs. - * * @param {string} project the Google Cloud project ID. * @return {Promise} a promise for an array of collection IDs. */ -export function listCollectionIds(project: string): Promise { +export function listCollectionIds( + project: string, + allowEmulator: boolean = false, +): Promise { + const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient; const url = "projects/" + project + "/databases/(default)/documents:listCollectionIds"; const data = { // Maximum 32-bit integer pageSize: 2147483647, }; - return _CLIENT.post(url, data).then((res) => { + return apiClient.post(url, data).then((res) => { return res.body.collectionIds || []; }); } @@ -30,12 +123,12 @@ export function listCollectionIds(project: string): Promise { * * For document format see: * https://firebase.google.com/docs/firestore/reference/rest/v1beta1/Document - * * @param {object} doc a Document object to delete. * @return {Promise} a promise for the delete operation. */ -export async function deleteDocument(doc: any): Promise { - return _CLIENT.delete(doc.name); +export async function deleteDocument(doc: any, allowEmulator: boolean = false): Promise { + const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient; + return apiClient.delete(doc.name); } /** @@ -43,12 +136,16 @@ export async function deleteDocument(doc: any): Promise { * * For document format see: * https://firebase.google.com/docs/firestore/reference/rest/v1beta1/Document - * * @param {string} project the Google Cloud project ID. * @param {object[]} docs an array of Document objects to delete. * @return {Promise} a promise for the number of deleted documents. */ -export async function deleteDocuments(project: string, docs: any[]): Promise { +export async function deleteDocuments( + project: string, + docs: any[], + allowEmulator: boolean = false, +): Promise { + const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient; const url = "projects/" + project + "/databases/(default)/documents:commit"; const writes = docs.map((doc) => { @@ -56,6 +153,123 @@ export async function deleteDocuments(project: string, docs: any[]): Promise(url, data); + const res = await apiClient.post(url, data); return res.body.writeResults.length; } + +/** + * Create a backup schedule for the given Firestore database. + * @param {string} project the Google Cloud project ID. + * @param {string} databaseId the Firestore database ID. + * @param {number} retention The retention of backups, in seconds. + * @param {Record?} dailyRecurrence Optional daily recurrence. + * @param {WeeklyRecurrence?} weeklyRecurrence Optional weekly recurrence. + */ +export async function createBackupSchedule( + project: string, + databaseId: string, + retention: number, + dailyRecurrence?: Record, + weeklyRecurrence?: WeeklyRecurrence, +): Promise { + const url = `projects/${project}/databases/${databaseId}/backupSchedules`; + const data = { + retention: durationFromSeconds(retention), + dailyRecurrence, + weeklyRecurrence, + }; + assertOneOf("BackupSchedule", data, "recurrence", "dailyRecurrence", "weeklyRecurrence"); + const res = await prodOnlyClient.post(url, data); + return res.body; +} + +/** + * Update a backup schedule for the given Firestore database. + * Only retention updates are currently supported. + * @param {string} backupScheduleName The backup schedule to update + * @param {number} retention The retention of backups, in seconds. + */ +export async function updateBackupSchedule( + backupScheduleName: string, + retention: number, +): Promise { + const data = { + retention: durationFromSeconds(retention), + }; + const res = await prodOnlyClient.patch(backupScheduleName, data); + return res.body; +} + +/** + * Delete a backup for the given Firestore database. + * @param {string} backupName Name of the backup + */ +export async function deleteBackup(backupName: string): Promise { + await prodOnlyClient.delete(backupName); +} + +/** + * Delete a backup schedule for the given Firestore database. + * @param {string} backupScheduleName Name of the backup schedule + */ +export async function deleteBackupSchedule(backupScheduleName: string): Promise { + await prodOnlyClient.delete(backupScheduleName); +} + +/** + * List all backups that exist at a given location. + * @param {string} project the Firebase project id. + * @param {string} location the Firestore location id. + */ +export async function listBackups(project: string, location: string): Promise { + const url = `/projects/${project}/locations/${location}/backups`; + const res = await prodOnlyClient.get(url); + return res.body; +} + +/** + * Get a backup + * @param {string} backupName the backup name + */ +export async function getBackup(backupName: string): Promise { + const res = await prodOnlyClient.get(backupName); + const backup = res.body; + if (!backup) { + throw new FirebaseError("Not found"); + } + + return backup; +} + +/** + * List all backup schedules that exist under a given database. + * @param {string} project the Firebase project id. + * @param {string} database the Firestore database id. + */ +export async function listBackupSchedules( + project: string, + database: string, +): Promise { + const url = `/projects/${project}/databases/${database}/backupSchedules`; + const res = await prodOnlyClient.get<{ backupSchedules?: BackupSchedule[] }>(url); + const backupSchedules = res.body.backupSchedules; + if (!backupSchedules) { + return []; + } + + return backupSchedules; +} + +/** + * Get a backup schedule + * @param {string} backupScheduleName Name of the backup schedule + */ +export async function getBackupSchedule(backupScheduleName: string): Promise { + const res = await prodOnlyClient.get(backupScheduleName); + const backupSchedule = res.body; + if (!backupSchedule) { + throw new FirebaseError("Not found"); + } + + return backupSchedule; +} diff --git a/src/gcp/iam.spec.ts b/src/gcp/iam.spec.ts new file mode 100644 index 00000000000..589f052a147 --- /dev/null +++ b/src/gcp/iam.spec.ts @@ -0,0 +1,104 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import { resourceManagerOrigin } from "../api"; +import * as iam from "./iam"; + +const BINDING = { + role: "some/role", + members: ["someuser"], +}; + +describe("iam", () => { + describe("mergeBindings", () => { + it("should not update the policy when the bindings are present", () => { + const policy = { + etag: "etag", + version: 3, + bindings: [BINDING], + }; + + const updated = iam.mergeBindings(policy, [BINDING]); + + expect(updated).to.be.false; + expect(policy.bindings).to.deep.equal([BINDING]); + }); + + it("should update the members of a binding in the policy", () => { + const policy = { + etag: "etag", + version: 3, + bindings: [BINDING], + }; + + const updated = iam.mergeBindings(policy, [{ role: "some/role", members: ["newuser"] }]); + + expect(updated).to.be.true; + expect(policy.bindings).to.deep.equal([ + { + role: "some/role", + members: ["someuser", "newuser"], + }, + ]); + }); + + it("should add a new binding to the policy", () => { + const policy = { + etag: "etag", + version: 3, + bindings: [], + }; + + const updated = iam.mergeBindings(policy, [BINDING]); + + expect(updated).to.be.true; + expect(policy.bindings).to.deep.equal([BINDING]); + }); + }); + + describe("testIamPermissions", () => { + const tests: { + desc: string; + permissionsToCheck: string[]; + permissionsToReturn: string[]; + wantAllowedPermissions: string[]; + wantMissingPermissions?: string[]; + wantedPassed: boolean; + }[] = [ + { + desc: "should pass if we have all permissions", + permissionsToCheck: ["foo", "bar"], + permissionsToReturn: ["foo", "bar"], + wantAllowedPermissions: ["foo", "bar"].sort(), + wantedPassed: true, + }, + { + desc: "should fail if we don't have all permissions", + permissionsToCheck: ["foo", "bar"], + permissionsToReturn: ["foo"], + wantAllowedPermissions: ["foo"].sort(), + wantMissingPermissions: ["bar"].sort(), + wantedPassed: false, + }, + ]; + + const TEST_RESOURCE = `projects/foo`; + + for (const t of tests) { + it(t.desc, async () => { + nock(resourceManagerOrigin()) + .post(`/v1/${TEST_RESOURCE}:testIamPermissions`) + .matchHeader("x-goog-quota-user", TEST_RESOURCE) + .reply(200, { permissions: t.permissionsToReturn }); + + const res = await iam.testIamPermissions("foo", t.permissionsToCheck); + + expect(res.allowed).to.deep.equal(t.wantAllowedPermissions); + expect(res.missing).to.deep.equal(t.wantMissingPermissions || []); + expect(res.passed).to.equal(t.wantedPassed); + + expect(nock.isDone()).to.be.true; + }); + } + }); +}); diff --git a/src/gcp/iam.ts b/src/gcp/iam.ts index 7ea41492ee7..6b722f1f43a 100644 --- a/src/gcp/iam.ts +++ b/src/gcp/iam.ts @@ -1,9 +1,19 @@ -import * as api from "../api"; -import { endpoint } from "../utils"; -import { difference } from "lodash"; +import { resourceManagerOrigin, iamOrigin } from "../api"; import { logger } from "../logger"; +import { Client } from "../apiv2"; +import * as utils from "../utils"; -const API_VERSION = "v1"; +const apiClient = new Client({ urlPrefix: iamOrigin(), apiVersion: "v1" }); + +/** Returns the default cloud build service agent */ +export function getDefaultCloudBuildServiceAgent(projectNumber: string): string { + return `${projectNumber}@cloudbuild.gserviceaccount.com`; +} + +/** Returns the default compute engine service agent */ +export function getDefaultComputeEngineServiceAgent(projectNumber: string): string { + return `${projectNumber}-compute@developer.gserviceaccount.com`; +} // IAM Policy // https://cloud.google.com/resource-manager/reference/rest/Shared.Types/Policy @@ -31,6 +41,12 @@ export interface ServiceAccount { disabled: boolean; } +export interface Role { + name: string; + title?: string; + description?: string; +} + export interface ServiceAccountKey { name: string; privateKeyType: string; @@ -43,9 +59,14 @@ export interface ServiceAccountKey { keyType: string; } +export interface TestIamResult { + allowed: string[]; + missing: string[]; + passed: boolean; +} + /** * Creates a new the service account with the given parameters. - * * @param projectId the id of the project where the service account will be created * @param accountId the id to use for the account * @param description a brief description of the account @@ -55,84 +76,81 @@ export async function createServiceAccount( projectId: string, accountId: string, description: string, - displayName: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): Promise { - const response = await api.request( - "POST", - `/${API_VERSION}/projects/${projectId}/serviceAccounts`, + displayName: string, +): Promise { + const response = await apiClient.post< + { accountId: string; serviceAccount: { displayName: string; description: string } }, + ServiceAccount + >( + `/projects/${projectId}/serviceAccounts`, { - auth: true, - origin: api.iamOrigin, - data: { - accountId, - serviceAccount: { - displayName, - description, - }, + accountId, + serviceAccount: { + displayName, + description, }, - } + }, + { skipLog: { resBody: true } }, ); return response.body; } /** * Retrieves a service account with the given parameters. - * * @param projectId the id of the project where the service account will be created * @param serviceAccountName the name of the service account */ export async function getServiceAccount( projectId: string, - serviceAccountName: string + serviceAccountName: string, ): Promise { - const response = await api.request( - "GET", - `/${API_VERSION}/projects/${projectId}/serviceAccounts/${serviceAccountName}@${projectId}.iam.gserviceaccount.com`, - { - auth: true, - origin: api.iamOrigin, - } + const response = await apiClient.get( + `/projects/${projectId}/serviceAccounts/${serviceAccountName}@${projectId}.iam.gserviceaccount.com`, ); return response.body; } +/** + * Creates a key for a given service account. + */ export async function createServiceAccountKey( projectId: string, - serviceAccountName: string + serviceAccountName: string, ): Promise { - const response = await api.request( - "POST", - `/${API_VERSION}/projects/${projectId}/serviceAccounts/${serviceAccountName}@${projectId}.iam.gserviceaccount.com/keys`, + const response = await apiClient.post< + { keyAlgorithm: string; privateKeyType: string }, + ServiceAccountKey + >( + `/projects/${projectId}/serviceAccounts/${serviceAccountName}@${projectId}.iam.gserviceaccount.com/keys`, { - auth: true, - origin: api.iamOrigin, - data: { - keyAlgorithm: "KEY_ALG_UNSPECIFIED", - privateKeyType: "TYPE_GOOGLE_CREDENTIALS_FILE", - }, - } + keyAlgorithm: "KEY_ALG_UNSPECIFIED", + privateKeyType: "TYPE_GOOGLE_CREDENTIALS_FILE", + }, ); return response.body; } /** - * * @param projectId the id of the project containing the service account * @param accountEmail the email of the service account to delete - * @return The raw API response, including status, body, etc. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function deleteServiceAccount(projectId: string, accountEmail: string): Promise { - return api.request( - "DELETE", - `/${API_VERSION}/projects/${projectId}/serviceAccounts/${accountEmail}`, - { - auth: true, - origin: api.iamOrigin, - resolveOnHTTPError: true, - } +export async function deleteServiceAccount(projectId: string, accountEmail: string): Promise { + await apiClient.delete(`/projects/${projectId}/serviceAccounts/${accountEmail}`, { + resolveOnHTTPError: true, + }); +} + +/** + * Lists every key for a given service account. + */ +export async function listServiceAccountKeys( + projectId: string, + serviceAccountName: string, +): Promise { + const response = await apiClient.get<{ keys: ServiceAccountKey[] }>( + `/projects/${projectId}/serviceAccounts/${serviceAccountName}@${projectId}.iam.gserviceaccount.com/keys`, ); + return response.body.keys; } /** @@ -142,24 +160,15 @@ export function deleteServiceAccount(projectId: string, accountEmail: string): P * @param role The IAM role to get, e.g. "editor". * @return Details about the IAM role. */ -export async function getRole(role: string): Promise<{ title: string; description: string }> { - const response = await api.request("GET", endpoint([API_VERSION, "roles", role]), { - auth: true, - origin: api.iamOrigin, +export async function getRole(role: string): Promise { + const response = await apiClient.get(`/roles/${role}`, { retryCodes: [500, 503], }); return response.body; } -export interface TestIamResult { - allowed: string[]; - missing: string[]; - passed: boolean; -} - /** * List permissions not held by an arbitrary resource implementing the IAM APIs. - * * @param origin Resource origin e.g. `https:// iam.googleapis.com`. * @param apiVersion API version e.g. `v1`. * @param resourceName Resource name e.g. `projects/my-projct/widgets/abc` @@ -169,30 +178,38 @@ export async function testResourceIamPermissions( origin: string, apiVersion: string, resourceName: string, - permissions: string[] + permissions: string[], + quotaUser = "", ): Promise { + const localClient = new Client({ urlPrefix: origin, apiVersion }); if (process.env.FIREBASE_SKIP_INFORMATIONAL_IAM) { logger.debug( - "[iam] skipping informational check of permissions", - JSON.stringify(permissions), - "on resource", - resourceName + `[iam] skipping informational check of permissions ${JSON.stringify( + permissions, + )} on resource ${resourceName}`, ); - return { allowed: permissions, missing: [], passed: true }; + return { allowed: Array.from(permissions).sort(), missing: [], passed: true }; } - const response = await api.request("POST", `/${apiVersion}/${resourceName}:testIamPermissions`, { - auth: true, - data: { permissions }, - origin, - }); + const headers: Record = {}; + if (quotaUser) { + headers["x-goog-quota-user"] = quotaUser; + } + const response = await localClient.post<{ permissions: string[] }, { permissions: string[] }>( + `/${resourceName}:testIamPermissions`, + { permissions }, + { headers }, + ); - const allowed = (response.body.permissions || []).sort(); - const missing = difference(permissions, allowed); + const allowed = new Set(response.body.permissions || []); + const missing = new Set(permissions); + for (const p of allowed) { + missing.delete(p); + } return { - allowed, - missing, - passed: missing.length === 0, + allowed: Array.from(allowed).sort(), + missing: Array.from(missing).sort(), + passed: missing.size === 0, }; } @@ -203,12 +220,62 @@ export async function testResourceIamPermissions( */ export async function testIamPermissions( projectId: string, - permissions: string[] + permissions: string[], ): Promise { return testResourceIamPermissions( - api.resourceManagerOrigin, + resourceManagerOrigin(), "v1", `projects/${projectId}`, - permissions + permissions, + `projects/${projectId}`, ); } + +/** Helper to merge all required bindings into the IAM policy, returns boolean if the policy has been updated */ +export function mergeBindings(policy: Policy, requiredBindings: Binding[]): boolean { + let updated = false; + for (const requiredBinding of requiredBindings) { + const match = policy.bindings.find((b) => b.role === requiredBinding.role); + if (!match) { + updated = true; + policy.bindings.push(requiredBinding); + continue; + } + for (const requiredMember of requiredBinding.members) { + if (!match.members.find((m) => m === requiredMember)) { + updated = true; + match.members.push(requiredMember); + } + } + } + return updated; +} + +/** Utility to print the required binding commands */ +export function printManualIamConfig( + requiredBindings: Binding[], + projectId: string, + prefix: string, +) { + utils.logLabeledBullet( + prefix, + "Failed to verify the project has the correct IAM bindings for a successful deployment.", + "warn", + ); + utils.logLabeledBullet( + prefix, + "You can either re-run this command as a project owner or manually run the following set of `gcloud` commands:", + "warn", + ); + for (const binding of requiredBindings) { + for (const member of binding.members) { + utils.logLabeledBullet( + prefix, + `\`gcloud projects add-iam-policy-binding ${projectId} ` + + `--member=${member} ` + + `--role=${binding.role}\``, + "warn", + ); + } + } +} diff --git a/src/gcp/identityPlatform.ts b/src/gcp/identityPlatform.ts new file mode 100644 index 00000000000..0be697f0923 --- /dev/null +++ b/src/gcp/identityPlatform.ts @@ -0,0 +1,221 @@ +import * as proto from "./proto"; +import { identityOrigin } from "../api"; +import { Client } from "../apiv2"; + +const API_VERSION = "v2"; + +const adminApiClient = new Client({ + urlPrefix: identityOrigin() + "/admin", + apiVersion: API_VERSION, +}); + +export type HashAlgorithm = + | "HASH_ALGORITHM_UNSPECIFIED" + | "HMAC_SHA256" + | "HMAC_SHA1" + | "HMAC_MD5" + | "SCRYPT" + | "PBKDF_SHA1" + | "MD5" + | "HMAC_SHA512" + | "SHA1" + | "BCRYPT" + | "PBKDF2_SHA256" + | "SHA256" + | "SHA512" + | "STANDARD_SCRYPT"; + +export interface EmailTemplate { + senderLocalPart: string; + subject: string; + senderDisplayName: string; + body: string; + bodyFormat: "BODY_FORMAT_UNSPECIFIED" | "PLAIN_TEXT" | "HTML"; + replyTo: string; + customized: boolean; +} + +export type Provider = "PROVIDER_UNSPECIFIED" | "PHONE_SMS"; + +export interface BlockingFunctionsConfig { + triggers?: { + beforeCreate?: BlockingFunctionsEventDetails; + beforeSignIn?: BlockingFunctionsEventDetails; + }; + forwardInboundCredentials?: BlockingFunctionsOptions; +} + +export interface BlockingFunctionsEventDetails { + functionUri?: string; + updateTime?: string; +} + +export interface BlockingFunctionsOptions { + idToken?: boolean; + accessToken?: boolean; + refreshToken?: boolean; +} + +export interface Config { + name?: string; + signIn?: { + email?: { + enabled: boolean; + passwordRequired: boolean; + }; + phoneNumber?: { + enabled: boolean; + testPhoneNumbers: Record; + }; + anonymous?: { + enabled: boolean; + }; + allowDuplicateEmails?: boolean; + hashConfig?: { + algorithm: HashAlgorithm; + signerKey: string; + saltSeparator: string; + rounds: number; + memoryCost: number; + }; + }; + notification?: { + sendEmail: { + method: "METHOD_UNSPECIFIED" | "DEFAULT" | "CUSTOM_SMTP"; + resetPasswordTemplate: EmailTemplate; + verifyEmailTemplate: EmailTemplate; + changeEmailTemplate: EmailTemplate; + legacyResetPasswordTemplate: EmailTemplate; + callbackUri: string; + dnsInfo: { + customDomain: string; + useCustomDomain: boolean; + pendingCustomDomain: string; + customDomainState: + | "VERIFICATION_STATE_UNSPECIFIED" + | "NOT_STARTED" + | "IN_PROGRESS" + | "FAILED" + | "SUCCEEDED"; + domainVerificationRequestTime: string; + }; + revertSecondFactorAdditionTemplate: EmailTemplate; + smtp: { + senderEmail: string; + host: string; + port: number; + username: string; + password: string; + securityMode: "SECURITY_MODE_UNSPECIFIED" | "SSL" | "START_TLS"; + }; + }; + sendSms: { + useDeviceLocale?: boolean; + smsTemplate?: { + content?: string; + }; + }; + defaultLocale?: string; + }; + quota?: { + signUpQuotaConfig?: { + quota?: string; + startTime?: string; + quotaDuration?: string; + }; + }; + monitoring?: { + requestLogging?: { + enabled?: boolean; + }; + }; + multiTenant?: { + allowTenants?: boolean; + defaultTenantLocation?: string; + }; + authorizedDomains?: Array; + subtype?: "SUBTYPE_UNSPECIFIED" | "IDENTITY_PLATFORM" | "FIREBASE_AUTH"; + client?: { + apiKey?: string; + permissions?: { + disabledUserSignup?: boolean; + disabledUserDeletion?: boolean; + }; + firebaseSubdomain?: string; + }; + mfa?: { + state?: "STATE_UNSPECIFIED" | "DISABLED" | "ENABLED" | "MANDATORY"; + enabledProviders?: Array; + }; + blockingFunctions?: BlockingFunctionsConfig; +} + +/** + * Helper function to get the blocking function config from identity platform. + * @param project GCP project ID or number + * @returns the blocking functions config + */ +export async function getBlockingFunctionsConfig( + project: string, +): Promise { + const config = (await getConfig(project)) || {}; + if (!config.blockingFunctions) { + config.blockingFunctions = {}; + } + return config.blockingFunctions; +} + +/** + * Gets the identity platform configuration. + * @param project GCP project ID or number + * @returns the identity platform config + */ +export async function getConfig(project: string): Promise { + const response = await adminApiClient.get(`projects/${project}/config`); + return response.body; +} + +/** + * Helper function to set the blocking function config to identity platform. + * @param project GCP project ID or number + * @param blockingConfig the blocking functions configuration to update + * @returns the blocking functions config + */ +export async function setBlockingFunctionsConfig( + project: string, + blockingConfig: BlockingFunctionsConfig, +): Promise { + const config = + (await updateConfig(project, { blockingFunctions: blockingConfig }, "blockingFunctions")) || {}; + if (!config.blockingFunctions) { + config.blockingFunctions = {}; + } + return config.blockingFunctions; +} + +/** + * Sets the identity platform configuration. + * @param project GCP project ID or number + * @param config the configuration to update + * @param updateMask optional update mask for the API + * @returns the updated config + */ +export async function updateConfig( + project: string, + config: Config, + updateMask?: string, +): Promise { + if (!updateMask) { + updateMask = proto.fieldMasks(config).join(","); + } + const response = await adminApiClient.patch( + `projects/${project}/config`, + config, + { + queryParams: { + updateMask, + }, + }, + ); + return response.body; +} diff --git a/src/gcp/index.js b/src/gcp/index.js deleted file mode 100644 index b205599bc6e..00000000000 --- a/src/gcp/index.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -module.exports = { - cloudbilling: require("./cloudbilling"), - cloudfunctions: require("./cloudfunctions"), - cloudscheduler: require("./cloudscheduler"), - cloudlogging: require("./cloudlogging"), - iam: require("./iam"), - pubsub: require("./pubsub"), - storage: require("./storage"), - rules: require("./rules"), -}; diff --git a/src/gcp/index.ts b/src/gcp/index.ts new file mode 100644 index 00000000000..0ce71736e97 --- /dev/null +++ b/src/gcp/index.ts @@ -0,0 +1,8 @@ +export * as cloudbilling from "./cloudbilling"; +export * as cloudfunctions from "./cloudfunctions"; +export * as cloudscheduler from "./cloudscheduler"; +export * as cloudlogging from "./cloudlogging"; +export * as iam from "./iam"; +export * as pubsub from "./pubsub"; +export * as storage from "./storage"; +export * as rules from "./rules"; diff --git a/src/test/gcp/proto.spec.ts b/src/gcp/proto.spec.ts similarity index 80% rename from src/test/gcp/proto.spec.ts rename to src/gcp/proto.spec.ts index 228bcaa4438..b03788575b4 100644 --- a/src/test/gcp/proto.spec.ts +++ b/src/gcp/proto.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as proto from "../../gcp/proto"; +import * as proto from "./proto"; describe("proto", () => { describe("duration", () => { @@ -79,9 +79,16 @@ describe("proto", () => { }); it("should support transformations", () => { + const dest: SrcType = {}; + const src: SrcType = { srcFoo: "baz" }; + proto.convertIfPresent(dest, src, "srcFoo", (str: string) => str + " transformed"); + expect(dest.srcFoo).to.equal("baz transformed"); + }); + + it("should support transformations with renames", () => { const dest: DestType = {}; const src: SrcType = { srcFoo: "baz" }; - proto.renameIfPresent(dest, src, "destFoo", "srcFoo", (str: string) => str + " transformed"); + proto.convertIfPresent(dest, src, "destFoo", "srcFoo", (str: string) => str + " transformed"); expect(dest.destFoo).to.equal("baz transformed"); }); @@ -101,7 +108,7 @@ describe("proto", () => { number: 1, string: "foo", array: ["hello", "world"], - }; + } as const; expect(proto.fieldMasks(obj).sort()).to.deep.equal(["number", "string", "array"].sort()); }); @@ -110,7 +117,7 @@ describe("proto", () => { undefined: undefined, null: null, empty: {}, - }; + } as const; expect(proto.fieldMasks(obj).sort()).to.deep.equal(["undefined", "null", "empty"].sort()); }); @@ -121,7 +128,7 @@ describe("proto", () => { nested: { key: "value", }, - }; + } as const; expect(proto.fieldMasks(obj).sort()).to.deep.equal(["top", "nested.key"].sort()); }); @@ -130,12 +137,14 @@ describe("proto", () => { failurePolicy: { retry: {}, }, - }; + } as const; expect(proto.fieldMasks(obj)).to.deep.equal(["failurePolicy.retry"]); }); it("should support map types", () => { - const obj = { + // Note: we need to erase type info, because the template args to fieldMasks + // will otherwise know that "missing" isn't possible and fail to compile. + const obj: Record = { map: { userDefined: "value", }, @@ -167,7 +176,7 @@ describe("proto", () => { it("should return formatted service accounts with invoker array", () => { const invokerMembers = proto.getInvokerMembers( ["service-account1@", "service-account2@project.iam.gserviceaccount.com"], - "project" + "project", ); expect(invokerMembers).to.deep.eq([ @@ -203,4 +212,42 @@ describe("proto", () => { expect(formatted).to.eq(`serviceAccount:${serviceAccount}`); }); }); + + it("pruneUndefindes", () => { + interface Interface { + foo?: string; + bar: string; + baz: { + alpha: Array; + bravo?: string; + charlie?: string; + }; + qux?: Record; + } + const src: Interface = { + foo: undefined, + bar: "bar", + baz: { + alpha: ["alpha", undefined], + bravo: undefined, + charlie: "charlie", + }, + qux: undefined, + }; + + const trimmed: Interface = { + bar: "bar", + baz: { + alpha: ["alpha"], + charlie: "charlie", + }, + }; + + // Show there is a problem + expect(src).to.not.deep.equal(trimmed); + + // Show we have the fix + proto.pruneUndefiends(src); + expect(src).to.deep.equal(trimmed); + }); }); diff --git a/src/gcp/proto.ts b/src/gcp/proto.ts index 21f4edcf50f..58afd50ea64 100644 --- a/src/gcp/proto.ts +++ b/src/gcp/proto.ts @@ -1,4 +1,5 @@ import { FirebaseError } from "../error"; +import { RecursiveKeyOf } from "../metaprogramming"; /** * A type alias used to annotate interfaces as using a google.protobuf.Duration. @@ -36,48 +37,108 @@ export function assertOneOf(typename: string, obj: T, oneof: string, ...field if (defined.length > 1) { throw new FirebaseError( `Invalid ${typename} definition. ${oneof} can only have one field defined, but found ${defined.join( - "," - )}` + ",", + )}`, ); } } -// eslint-disable @typescript-eslint/no-unsafe-returns @typescript-eslint/no-explicit-any - /** * Utility function to help copy fields from type A to B. * As a safety net, catches typos or fields that aren't named the same * in A and B, but cannot verify that both Src and Dest have the same type for the same field. */ -export function copyIfPresent( +export function copyIfPresent( dest: Dest, - src: Src, - ...fields: (keyof Src & keyof Dest)[] -) { + src: { [Key in Keys[number]]?: Dest[Key] }, + ...fields: Keys +): void { for (const field of fields) { if (!Object.prototype.hasOwnProperty.call(src, field)) { continue; } - dest[field] = src[field] as any; + dest[field] = src[field]!; } } -export function renameIfPresent( +/** + * Utility function to help convert a field from type A to B if they are present. + */ +export function convertIfPresent< + Dest extends object, + Src extends object, + Key extends keyof Src & keyof Dest, +>( dest: Dest, src: Src, - destField: keyof Dest, - srcField: keyof Src, - converter: (from: any) => any = (from: any) => { - return from; - } -) { - if (!Object.prototype.hasOwnProperty.call(src, srcField)) { + key: Key, + converter: (from: Required[Key]) => Required[Key], +): void; + +/** + * Utility function to help convert a field from type A to type B while renaming. + */ +export function convertIfPresent< + Dest extends object, + Src extends object, + DestKey extends keyof Dest, + SrcKey extends keyof Src, +>( + dest: Dest, + src: Src, + destKey: DestKey, + srcKey: SrcKey, + converter: (from: Required[SrcKey]) => Required[DestKey], +): void; + +/** Overload */ +export function convertIfPresent< + Dest extends object, + Src extends object, + DestKey extends keyof Dest, + SrcKey extends keyof Src, + MutualKey extends keyof Dest & keyof Src, +>( + ...args: + | [ + dest: Dest, + src: Src, + key: MutualKey, + converter: (from: Required[MutualKey]) => Required[MutualKey], + ] + | [ + dest: Dest, + src: Src, + destKey: DestKey, + srcKey: SrcKey, + converter: (from: Required[SrcKey]) => Required[DestKey], + ] +): void { + if (args.length === 4) { + const [dest, src, key, converter] = args; + if (Object.prototype.hasOwnProperty.call(src, key)) { + dest[key] = converter(src[key]); + } return; } - dest[destField] = converter(src[srcField]); + const [dest, src, destKey, srcKey, converter] = args; + if (Object.prototype.hasOwnProperty.call(src, srcKey)) { + dest[destKey] = converter(src[srcKey]); + } } -// eslint-enable @typescript-eslint/no-unsafe-returns @typescript-eslint/no-explicit-any +/** Moves a field from one key in source to another key in dest */ +export function renameIfPresent( + dest: { [Key in DestKey]?: ValType }, + src: { [Key in SrcKey]?: ValType }, + destKey: DestKey, + srcKey: SrcKey, +): void { + if (!Object.prototype.hasOwnProperty.call(src, srcKey)) { + return; + } + dest[destKey] = src[srcKey]; +} /** * Calculate a field mask of all values set in object. @@ -89,7 +150,10 @@ export function renameIfPresent( * @param doNotRecurseIn the dot-delimited address of fields which, if present, are proto map * types and their keys are not part of the field mask. */ -export function fieldMasks(object: Record, ...doNotRecurseIn: string[]): string[] { +export function fieldMasks( + object: T, + ...doNotRecurseIn: Array> +): string[] { const masks: string[] = []; fieldMasksHelper([], object, doNotRecurseIn, masks); return masks; @@ -99,9 +163,15 @@ function fieldMasksHelper( prefixes: string[], cursor: unknown, doNotRecurseIn: string[], - masks: string[] -) { - if (typeof cursor !== "object" || Array.isArray(cursor) || cursor === null) { + masks: string[], +): void { + // Empty arrays should never be sent because they're dropped by the one platform + // gateway and then services get confused why there's an update mask for a missing field" + if (Array.isArray(cursor) && !cursor.length) { + return; + } + + if (typeof cursor !== "object" || (Array.isArray(cursor) && cursor.length) || cursor === null) { masks.push(prefixes.join(".")); return; } @@ -128,8 +198,7 @@ function fieldMasksHelper( * Gets the correctly invoker members to be used with the invoker role for IAM API calls. * @param invoker the array of non-formatted invoker members * @param projectId the ID of the current project - * @returns an array of correctly formatted invoker members - * + * @return an array of correctly formatted invoker members * @throws {@link FirebaseError} if any invoker string is empty or not of the correct form */ export function getInvokerMembers(invoker: string[], projectId: string): string[] { @@ -147,8 +216,7 @@ export function getInvokerMembers(invoker: string[], projectId: string): string[ * '{service-account}@' or '{service-account}@{project}.iam.gserviceaccount.com'. * @param serviceAccount the custom service account created by the user * @param projectId the ID of the current project - * @returns a correctly formatted service account string - * + * @return a correctly formatted service account string * @throws {@link FirebaseError} if the supplied service account string is empty or not of the correct form */ export function formatServiceAccount(serviceAccount: string, projectId: string): string { @@ -157,7 +225,7 @@ export function formatServiceAccount(serviceAccount: string, projectId: string): } if (!serviceAccount.includes("@")) { throw new FirebaseError( - "Service account must be of the form 'service-account@' or 'service-account@{project-id}.iam.gserviceaccount.com'" + "Service account must be of the form 'service-account@' or 'service-account@{project-id}.iam.gserviceaccount.com'", ); } @@ -167,3 +235,30 @@ export function formatServiceAccount(serviceAccount: string, projectId: string): } return `serviceAccount:${serviceAccount}`; } + +/** + * Remove keys whose values are undefined. + * When we write an interface { foo?: number } there are three possible + * forms: { foo: 1 }, {}, and { foo: undefined }. The latter surprises + * most people and make unit test comparison flaky. This cleans that up. + */ +export function pruneUndefiends(obj: unknown): void { + if (typeof obj !== "object" || obj === null) { + return; + } + const keyable = obj as Record; + for (const key of Object.keys(keyable)) { + if (keyable[key] === undefined) { + delete keyable[key]; + } else if (typeof keyable[key] === "object") { + if (Array.isArray(keyable[key])) { + for (const sub of keyable[key] as unknown[]) { + pruneUndefiends(sub); + } + keyable[key] = (keyable[key] as unknown[]).filter((e) => e !== undefined); + } else { + pruneUndefiends(keyable[key]); + } + } + } +} diff --git a/src/gcp/pubsub.ts b/src/gcp/pubsub.ts index eb6d2617739..4958435c6f6 100644 --- a/src/gcp/pubsub.ts +++ b/src/gcp/pubsub.ts @@ -1,12 +1,11 @@ import { Client } from "../apiv2"; import { pubsubOrigin } from "../api"; -import * as backend from "../deploy/functions/backend"; import * as proto from "./proto"; const API_VERSION = "v1"; const client = new Client({ - urlPrefix: pubsubOrigin, + urlPrefix: pubsubOrigin(), auth: true, apiVersion: API_VERSION, }); diff --git a/src/gcp/resourceManager.ts b/src/gcp/resourceManager.ts index cc58e0345a1..e1d08bd17d0 100644 --- a/src/gcp/resourceManager.ts +++ b/src/gcp/resourceManager.ts @@ -1,28 +1,32 @@ import { findIndex } from "lodash"; -import * as api from "../api"; +import { resourceManagerOrigin } from "../api"; +import { Client } from "../apiv2"; import { Binding, getServiceAccount, Policy } from "./iam"; const API_VERSION = "v1"; +const apiClient = new Client({ urlPrefix: resourceManagerOrigin(), apiVersion: API_VERSION }); + // Roles listed at https://firebase.google.com/docs/projects/iam/roles-predefined-product export const firebaseRoles = { apiKeysViewer: "roles/serviceusage.apiKeysViewer", authAdmin: "roles/firebaseauth.admin", + functionsDeveloper: "roles/cloudfunctions.developer", hostingAdmin: "roles/firebasehosting.admin", runViewer: "roles/run.viewer", + serviceUsageConsumer: "roles/serviceusage.serviceUsageConsumer", }; /** * Fetches the IAM Policy of a project. * https://cloud.google.com/resource-manager/reference/rest/v1/projects/getIamPolicy * - * @param projectId the id of the project whose IAM Policy you want to get + * @param projectIdOrNumber the id of the project whose IAM Policy you want to get */ -export async function getIamPolicy(projectId: string): Promise { - const response = await api.request("POST", `/${API_VERSION}/projects/${projectId}:getIamPolicy`, { - auth: true, - origin: api.resourceManagerOrigin, - }); +export async function getIamPolicy(projectIdOrNumber: string): Promise { + const response = await apiClient.post( + `/projects/${projectIdOrNumber}:getIamPolicy`, + ); return response.body; } @@ -30,23 +34,22 @@ export async function getIamPolicy(projectId: string): Promise { * Sets the IAM Policy of a project. * https://cloud.google.com/resource-manager/reference/rest/v1/projects/setIamPolicy * - * @param projectId the id of the project for which you want to set a new IAM Policy + * @param projectIdOrNumber the id of the project for which you want to set a new IAM Policy * @param newPolicy the new IAM policy for the project * @param updateMask A FieldMask specifying which fields of the policy to modify */ export async function setIamPolicy( - projectId: string, + projectIdOrNumber: string, newPolicy: Policy, - updateMask?: string + updateMask = "", ): Promise { - const response = await api.request("POST", `/${API_VERSION}/projects/${projectId}:setIamPolicy`, { - auth: true, - origin: api.resourceManagerOrigin, - data: { + const response = await apiClient.post<{ policy: Policy; updateMask: string }, Policy>( + `/projects/${projectIdOrNumber}:setIamPolicy`, + { policy: newPolicy, updateMask: updateMask, }, - }); + ); return response.body; } @@ -60,10 +63,13 @@ export async function setIamPolicy( export async function addServiceAccountToRoles( projectId: string, serviceAccountName: string, - roles: string[] + roles: string[], + skipAccountLookup = false, ): Promise { const [{ name: fullServiceAccountName }, projectPolicy] = await Promise.all([ - getServiceAccount(projectId, serviceAccountName), + skipAccountLookup + ? Promise.resolve({ name: serviceAccountName }) + : getServiceAccount(projectId, serviceAccountName), getIamPolicy(projectId), ]); @@ -75,7 +81,7 @@ export async function addServiceAccountToRoles( roles.forEach((roleName) => { let bindingIndex = findIndex( projectPolicy.bindings, - (binding: Binding) => binding.role === roleName + (binding: Binding) => binding.role === roleName, ); // create a new binding if the role doesn't exist in the policy yet @@ -97,3 +103,36 @@ export async function addServiceAccountToRoles( return setIamPolicy(projectId, projectPolicy, "bindings"); } + +export async function serviceAccountHasRoles( + projectId: string, + serviceAccountName: string, + roles: string[], + skipAccountLookup = false, +): Promise { + const [{ name: fullServiceAccountName }, projectPolicy] = await Promise.all([ + skipAccountLookup + ? Promise.resolve({ name: serviceAccountName }) + : getServiceAccount(projectId, serviceAccountName), + getIamPolicy(projectId), + ]); + + // The way the service account name is formatted in the Policy object + // https://cloud.google.com/iam/docs/reference/rest/v1/Policy + // serviceAccount:my-project-id@appspot.gserviceaccount.com + const memberName = `serviceAccount:${fullServiceAccountName.split("/").pop()}`; + + for (const roleName of roles) { + const binding = projectPolicy.bindings.find((b: Binding) => b.role === roleName); + if (!binding) { + return false; + } + + // No need to update if service account already has role + if (!binding.members.includes(memberName)) { + return false; + } + } + + return true; +} diff --git a/src/gcp/rules.ts b/src/gcp/rules.ts index 0a2bf466385..c6fc3af362a 100644 --- a/src/gcp/rules.ts +++ b/src/gcp/rules.ts @@ -1,11 +1,12 @@ -import * as _ from "lodash"; - -import * as api from "../api"; +import { rulesOrigin } from "../api"; +import { Client } from "../apiv2"; import { logger } from "../logger"; import * as utils from "../utils"; const API_VERSION = "v1"; +const apiClient = new Client({ urlPrefix: rulesOrigin(), apiVersion: API_VERSION }); + function _handleErrorResponse(response: any): any { if (response.body && response.body.error) { return utils.reject(response.body.error, { code: 2 }); @@ -21,15 +22,15 @@ function _handleErrorResponse(response: any): any { * Gets the latest ruleset name on the project. * @param projectId Project from which you want to get the ruleset. * @param service Service for the ruleset (ex: cloud.firestore or firebase.storage). - * @returns Name of the latest ruleset. + * @return Name of the latest ruleset. */ export async function getLatestRulesetName( projectId: string, - service: string + service: string, ): Promise { const releases = await listAllReleases(projectId); const prefix = `projects/${projectId}/releases/${service}`; - const release = _.find(releases, (r) => r.name.indexOf(prefix) === 0); + const release = releases.find((r) => r.name.startsWith(prefix)); if (!release) { return null; @@ -44,12 +45,10 @@ const MAX_RELEASES_PAGE_SIZE = 10; */ export async function listReleases( projectId: string, - pageToken?: string + pageToken = "", ): Promise { - const response = await api.request("GET", `/${API_VERSION}/projects/${projectId}/releases`, { - auth: true, - origin: api.rulesOrigin, - query: { + const response = await apiClient.get(`/projects/${projectId}/releases`, { + queryParams: { pageSize: MAX_RELEASES_PAGE_SIZE, pageToken, }, @@ -87,7 +86,7 @@ export async function listAllReleases(projectId: string): Promise { } pageToken = response.nextPageToken; } while (pageToken); - return _.orderBy(releases, ["createTime"], ["desc"]); + return releases.sort((a, b) => b.createTime.localeCompare(a.createTime)); } export interface RulesetFile { @@ -105,12 +104,11 @@ export interface RulesetSource { * @return Array of files in the ruleset. Each entry has form { content, name }. */ export async function getRulesetContent(name: string): Promise { - const response = await api.request("GET", `/${API_VERSION}/${name}`, { - auth: true, - origin: api.rulesOrigin, + const response = await apiClient.get<{ source: RulesetSource }>(`/${name}`, { + skipLog: { resBody: true }, }); if (response.status === 200) { - const source: RulesetSource = response.body.source; + const source = response.body.source; return source.files; } @@ -124,15 +122,14 @@ const MAX_RULESET_PAGE_SIZE = 100; */ export async function listRulesets( projectId: string, - pageToken?: string + pageToken: string = "", ): Promise { - const response = await api.request("GET", `/${API_VERSION}/projects/${projectId}/rulesets`, { - auth: true, - origin: api.rulesOrigin, - query: { + const response = await apiClient.get(`/projects/${projectId}/rulesets`, { + queryParams: { pageSize: MAX_RULESET_PAGE_SIZE, pageToken, }, + skipLog: { resBody: true }, }); if (response.status === 200) { return response.body; @@ -155,7 +152,7 @@ export async function listAllRulesets(projectId: string): Promise b.createTime.localeCompare(a.createTime)); } export interface ListRulesetsResponse { @@ -178,14 +175,7 @@ export function getRulesetId(ruleset: ListRulesetsEntry): string { * by a release, the operation will fail. */ export async function deleteRuleset(projectId: string, id: string): Promise { - const response = await api.request( - "DELETE", - `/${API_VERSION}/projects/${projectId}/rulesets/${id}`, - { - auth: true, - origin: api.rulesOrigin, - } - ); + const response = await apiClient.delete(`/projects/${projectId}/rulesets/${id}`); if (response.status === 200) { return; } @@ -200,11 +190,11 @@ export async function deleteRuleset(projectId: string, id: string): Promise { const payload = { source: { files } }; - const response = await api.request("POST", `/${API_VERSION}/projects/${projectId}/rulesets`, { - auth: true, - data: payload, - origin: api.rulesOrigin, - }); + const response = await apiClient.post( + `/projects/${projectId}/rulesets`, + payload, + { skipLog: { body: true } }, + ); if (response.status === 200) { logger.debug("[rules] created ruleset", response.body.name); return response.body.name; @@ -222,18 +212,17 @@ export async function createRuleset(projectId: string, files: RulesetFile[]): Pr export async function createRelease( projectId: string, rulesetName: string, - releaseName: string + releaseName: string, ): Promise { const payload = { name: `projects/${projectId}/releases/${releaseName}`, rulesetName, }; - const response = await api.request("POST", `/${API_VERSION}/projects/${projectId}/releases`, { - auth: true, - data: payload, - origin: api.rulesOrigin, - }); + const response = await apiClient.post( + `/projects/${projectId}/releases`, + payload, + ); if (response.status === 200) { logger.debug("[rules] created release", response.body.name); return response.body.name; @@ -251,7 +240,7 @@ export async function createRelease( export async function updateRelease( projectId: string, rulesetName: string, - releaseName: string + releaseName: string, ): Promise { const payload = { release: { @@ -260,14 +249,9 @@ export async function updateRelease( }, }; - const response = await api.request( - "PATCH", - `/${API_VERSION}/projects/${projectId}/releases/${releaseName}`, - { - auth: true, - data: payload, - origin: api.rulesOrigin, - } + const response = await apiClient.patch( + `/projects/${projectId}/releases/${releaseName}`, + payload, ); if (response.status === 200) { logger.debug("[rules] updated release", response.body.name); @@ -280,7 +264,7 @@ export async function updateRelease( export async function updateOrCreateRelease( projectId: string, rulesetName: string, - releaseName: string + releaseName: string, ): Promise { logger.debug("[rules] releasing", releaseName, "with ruleset", rulesetName); return updateRelease(projectId, rulesetName, releaseName).catch(() => { @@ -290,11 +274,9 @@ export async function updateOrCreateRelease( } export function testRuleset(projectId: string, files: RulesetFile[]): Promise { - return api.request("POST", `/${API_VERSION}/projects/${encodeURIComponent(projectId)}:test`, { - origin: api.rulesOrigin, - data: { - source: { files }, - }, - auth: true, - }); + return apiClient.post( + `/projects/${encodeURIComponent(projectId)}:test`, + { source: { files } }, + { skipLog: { body: true } }, + ); } diff --git a/src/gcp/run.spec.ts b/src/gcp/run.spec.ts new file mode 100644 index 00000000000..3661401fcdc --- /dev/null +++ b/src/gcp/run.spec.ts @@ -0,0 +1,455 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as run from "./run"; +import { Client } from "../apiv2"; + +describe("run", () => { + describe("setInvokerCreate", () => { + let sandbox: sinon.SinonSandbox; + let apiRequestStub: sinon.SinonStub; + let client: Client; + + beforeEach(() => { + client = new Client({ + urlPrefix: "origin", + auth: true, + apiVersion: "v1", + }); + sandbox = sinon.createSandbox(); + apiRequestStub = sandbox.stub(client, "post").throws("Unexpected API post call"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should reject on emtpy invoker array", async () => { + await expect(run.setInvokerCreate("project", "service", [], client)).to.be.rejected; + }); + + it("should reject if the setting the IAM policy fails", async () => { + apiRequestStub.onFirstCall().throws("Error calling set api."); + + await expect( + run.setInvokerCreate("project", "service", ["public"], client), + ).to.be.rejectedWith("Failed to set the IAM Policy on the Service service"); + expect(apiRequestStub).to.be.calledOnce; + }); + + it("should set a private policy on a function", async () => { + apiRequestStub.onFirstCall().callsFake((path: string, json: any) => { + expect(json.policy).to.deep.eq({ + bindings: [ + { + role: "roles/run.invoker", + members: [], + }, + ], + etag: "", + version: 3, + }); + + return Promise.resolve(); + }); + + await expect(run.setInvokerCreate("project", "service", ["private"], client)).to.not.be + .rejected; + expect(apiRequestStub).to.be.calledOnce; + }); + + it("should set a public policy on a function", async () => { + apiRequestStub.onFirstCall().callsFake((path: string, json: any) => { + expect(json.policy).to.deep.eq({ + bindings: [ + { + role: "roles/run.invoker", + members: ["allUsers"], + }, + ], + etag: "", + version: 3, + }); + + return Promise.resolve(); + }); + + await expect(run.setInvokerCreate("project", "service", ["public"], client)).to.not.be + .rejected; + expect(apiRequestStub).to.be.calledOnce; + }); + + it("should set the policy with a set of invokers with active policies", async () => { + apiRequestStub.onFirstCall().callsFake((path: string, json: any) => { + json.policy.bindings[0].members.sort(); + expect(json.policy.bindings[0].members).to.deep.eq([ + "serviceAccount:service-account1@project.iam.gserviceaccount.com", + "serviceAccount:service-account2@project.iam.gserviceaccount.com", + "serviceAccount:service-account3@project.iam.gserviceaccount.com", + ]); + + return Promise.resolve(); + }); + + await expect( + run.setInvokerCreate( + "project", + "service", + [ + "service-account1@", + "service-account2@project.iam.gserviceaccount.com", + "service-account3@", + ], + client, + ), + ).to.not.be.rejected; + expect(apiRequestStub).to.be.calledOnce; + }); + }); + + describe("setInvokerUpdate", () => { + describe("setInvokerCreate", () => { + let sandbox: sinon.SinonSandbox; + let apiPostStub: sinon.SinonStub; + let apiGetStub: sinon.SinonStub; + let client: Client; + + beforeEach(() => { + client = new Client({ + urlPrefix: "origin", + auth: true, + apiVersion: "v1", + }); + sandbox = sinon.createSandbox(); + apiPostStub = sandbox.stub(client, "post").throws("Unexpected API post call"); + apiGetStub = sandbox.stub(client, "get").throws("Unexpected API get call"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should reject on emtpy invoker array", async () => { + await expect(run.setInvokerUpdate("project", "service", [])).to.be.rejected; + }); + + it("should reject if the getting the IAM policy fails", async () => { + apiGetStub.onFirstCall().throws("Error calling get api."); + + await expect( + run.setInvokerUpdate("project", "service", ["public"], client), + ).to.be.rejectedWith("Failed to get the IAM Policy on the Service service"); + + expect(apiGetStub).to.be.called; + }); + + it("should reject if the setting the IAM policy fails", async () => { + apiGetStub.resolves({ body: {} }); + apiPostStub.throws("Error calling set api."); + + await expect( + run.setInvokerUpdate("project", "service", ["public"], client), + ).to.be.rejectedWith("Failed to set the IAM Policy on the Service service"); + expect(apiGetStub).to.be.calledOnce; + expect(apiPostStub).to.be.calledOnce; + }); + + it("should set a basic policy on a function without any polices", async () => { + apiGetStub.onFirstCall().resolves({ body: {} }); + apiPostStub.onFirstCall().callsFake((path: string, json: any) => { + expect(json.policy).to.deep.eq({ + bindings: [ + { + role: "roles/run.invoker", + members: ["allUsers"], + }, + ], + etag: "", + version: 3, + }); + + return Promise.resolve(); + }); + + await expect(run.setInvokerUpdate("project", "service", ["public"], client)).to.not.be + .rejected; + expect(apiGetStub).to.be.calledOnce; + expect(apiPostStub).to.be.calledOnce; + }); + + it("should set the policy with private invoker with active policies", async () => { + apiGetStub.onFirstCall().resolves({ + body: { + bindings: [ + { role: "random-role", members: ["user:pineapple"] }, + { role: "roles/run.invoker", members: ["some-service-account"] }, + ], + etag: "1234", + version: 3, + }, + }); + apiPostStub.onFirstCall().callsFake((path: string, json: any) => { + expect(json.policy).to.deep.eq({ + bindings: [ + { role: "random-role", members: ["user:pineapple"] }, + { role: "roles/run.invoker", members: [] }, + ], + etag: "1234", + version: 3, + }); + + return Promise.resolve(); + }); + + await expect(run.setInvokerUpdate("project", "service", ["private"], client)).to.not.be + .rejected; + expect(apiGetStub).to.be.calledOnce; + expect(apiPostStub).to.be.calledOnce; + }); + + it("should set the policy with a set of invokers with active policies", async () => { + apiGetStub.onFirstCall().resolves({ body: {} }); + apiPostStub.onFirstCall().callsFake((path: string, json: any) => { + json.policy.bindings[0].members.sort(); + expect(json.policy.bindings[0].members).to.deep.eq([ + "serviceAccount:service-account1@project.iam.gserviceaccount.com", + "serviceAccount:service-account2@project.iam.gserviceaccount.com", + "serviceAccount:service-account3@project.iam.gserviceaccount.com", + ]); + + return Promise.resolve(); + }); + + await expect( + run.setInvokerUpdate( + "project", + "service", + [ + "service-account1@", + "service-account2@project.iam.gserviceaccount.com", + "service-account3@", + ], + client, + ), + ).to.not.be.rejected; + expect(apiGetStub).to.be.calledOnce; + expect(apiPostStub).to.be.calledOnce; + }); + + it("should not set the policy if the set of invokers is the same as the current invokers", async () => { + apiGetStub.onFirstCall().resolves({ + body: { + bindings: [ + { + role: "roles/run.invoker", + members: [ + "serviceAccount:service-account1@project.iam.gserviceaccount.com", + "serviceAccount:service-account3@project.iam.gserviceaccount.com", + "serviceAccount:service-account2@project.iam.gserviceaccount.com", + ], + }, + ], + etag: "1234", + version: 3, + }, + }); + + await expect( + run.setInvokerUpdate( + "project", + "service", + [ + "service-account2@project.iam.gserviceaccount.com", + "service-account3@", + "service-account1@", + ], + client, + ), + ).to.not.be.rejected; + expect(apiGetStub).to.be.calledOnce; + expect(apiPostStub).to.not.be.called; + }); + }); + }); + describe("updateService", () => { + let service: run.Service; + let serviceIsResolved: sinon.SinonStub; + let replaceService: sinon.SinonStub; + let getService: sinon.SinonStub; + + beforeEach(() => { + serviceIsResolved = sinon + .stub(run, "serviceIsResolved") + .throws(new Error("Unexpected serviceIsResolved call")); + replaceService = sinon + .stub(run, "replaceService") + .throws(new Error("Unexpected replaceService call")); + getService = sinon.stub(run, "getService").throws(new Error("Unexpected getService call")); + + service = { + apiVersion: "serving.knative.dev/v1", + kind: "Service", + metadata: { + name: "service", + namespace: "project", + }, + spec: { + template: { + metadata: { + name: "service", + namespace: "project", + }, + spec: { + containerConcurrency: 1, + containers: [ + { + image: "image", + ports: [ + { + name: "main", + containerPort: 8080, + }, + ], + env: {}, + resources: { + limits: { + memory: "256M", + cpu: "0.1667", + }, + }, + }, + ], + }, + }, + traffic: [], + }, + }; + }); + + afterEach(() => { + serviceIsResolved.restore(); + getService.restore(); + replaceService.restore(); + }); + + it("handles noops immediately", async () => { + replaceService.resolves(service); + getService.resolves(service); + serviceIsResolved.returns(true); + await run.updateService("name", service); + + expect(replaceService).to.have.been.calledOnce; + expect(serviceIsResolved).to.have.been.calledOnce; + expect(getService).to.not.have.been.called; + }); + + it("loops on ready status", async () => { + replaceService.resolves(service); + getService.resolves(service); + serviceIsResolved.onFirstCall().returns(false); + serviceIsResolved.onSecondCall().returns(true); + await run.updateService("name", service); + + expect(replaceService).to.have.been.calledOnce; + expect(serviceIsResolved).to.have.been.calledTwice; + expect(getService).to.have.been.calledOnce; + }); + }); + + describe("serviceIsResolved", () => { + let service: run.Service; + beforeEach(() => { + service = { + apiVersion: "serving.knative.dev/v1", + kind: "Service", + metadata: { + name: "service", + namespace: "project", + generation: 2, + }, + spec: { + template: { + metadata: { + name: "service", + namespace: "project", + }, + spec: { + containerConcurrency: 1, + containers: [ + { + image: "image", + ports: [ + { + name: "main", + containerPort: 8080, + }, + ], + env: {}, + resources: { + limits: { + memory: "256M", + cpu: "0.1667", + }, + }, + }, + ], + }, + }, + traffic: [], + }, + status: { + observedGeneration: 2, + conditions: [ + { + status: "True", + type: "Ready", + reason: "Testing", + lastTransitionTime: "", + message: "", + severity: "Info", + }, + ], + latestCreatedRevisionName: "", + latestReadyRevisionName: "", + traffic: [], + url: "", + address: { + url: "", + }, + }, + }; + }); + + it("returns false if the observed generation isn't the metageneration", () => { + service.status!.observedGeneration = 1; + service.metadata.generation = 2; + expect(run.serviceIsResolved(service)).to.be.false; + }); + + it("returns false if the status is not ready", () => { + service.status!.observedGeneration = 2; + service.metadata.generation = 2; + service.status!.conditions[0].status = "Unknown"; + service.status!.conditions[0].type = "Ready"; + + expect(run.serviceIsResolved(service)).to.be.false; + }); + + it("throws if we have an failed status", () => { + service.status!.observedGeneration = 2; + service.metadata.generation = 2; + service.status!.conditions[0].status = "False"; + service.status!.conditions[0].type = "Ready"; + + expect(() => run.serviceIsResolved(service)).to.throw; + }); + + it("returns true if resolved", () => { + service.status!.observedGeneration = 2; + service.metadata.generation = 2; + service.status!.conditions[0].status = "True"; + service.status!.conditions[0].type = "Ready"; + + expect(run.serviceIsResolved(service)).to.be.true; + }); + }); +}); diff --git a/src/gcp/run.ts b/src/gcp/run.ts index 25a19f6682a..286f12e82f9 100644 --- a/src/gcp/run.ts +++ b/src/gcp/run.ts @@ -3,12 +3,13 @@ import { FirebaseError } from "../error"; import { runOrigin } from "../api"; import * as proto from "./proto"; import * as iam from "./iam"; -import * as _ from "lodash"; +import { backoff } from "../throttler/throttler"; +import { logger } from "../logger"; const API_VERSION = "v1"; const client = new Client({ - urlPrefix: runOrigin, + urlPrefix: runOrigin(), auth: true, apiVersion: API_VERSION, }); @@ -67,7 +68,7 @@ export interface ServiceSpec { export interface ServiceStatus { observedGeneration: number; conditions: Condition[]; - latestRevisionName: string; + latestReadyRevisionName: string; latestCreatedRevisionName: string; traffic: TrafficTarget[]; url: string; @@ -76,28 +77,41 @@ export interface ServiceStatus { export interface Service { apiVersion: "serving.knative.dev/v1"; - kind: "service"; + kind: "Service"; metadata: ObjectMetadata; spec: ServiceSpec; status?: ServiceStatus; } +export interface Container { + image: string; + ports: Array<{ name: string; containerPort: number }>; + env: Record; + resources: { + limits: { + cpu: string; + memory: string; + }; + }; +} + export interface RevisionSpec { containerConcurrency?: number | null; + containers: Container[]; } export interface RevisionTemplate { - metadata: ObjectMetadata; + metadata: Partial; spec: RevisionSpec; } export interface TrafficTarget { - configurationName: string; + configurationName?: string; // RevisionName can be used to target a specific revision, // or customers can set latestRevision = true revisionName?: string; latestRevision?: boolean; - percent: number; + percent?: number; // optional when tagged tag?: string; // Output only: @@ -113,24 +127,101 @@ export interface IamPolicy { etag?: string; } +export interface GCPIds { + serviceId: string; + region: string; + projectNumber: string; +} + +/** + * Gets the standard project/location/id tuple from the K8S style resource. + */ +export function gcpIds(service: Pick): GCPIds { + return { + serviceId: service.metadata.name, + projectNumber: service.metadata.namespace, + region: service.metadata.labels?.[LOCATION_LABEL] || "unknown-region", + }; +} +/** + * Gets a service with a given name. + */ export async function getService(name: string): Promise { try { const response = await client.get(name); return response.body; - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to fetch Run service ${name}`, { original: err, + status: err?.context?.response?.statusCode, }); } } +/** + * Update a service and wait for changes to replicate. + */ +export async function updateService(name: string, service: Service): Promise { + delete service.status; + service = await exports.replaceService(name, service); + + // Now we need to wait for reconciliation or we might delete the docker + // image while the service is still rolling out a new revision. + let retry = 0; + while (!exports.serviceIsResolved(service)) { + await backoff(retry, 2, 30); + retry = retry + 1; + service = await exports.getService(name); + } + return service; +} + +/** + * Returns whether a service is resolved (all transitions have completed). + */ +export function serviceIsResolved(service: Service): boolean { + if (service.status?.observedGeneration !== service.metadata.generation) { + logger.debug( + `Service ${service.metadata.name} is not resolved because` + + `observed generation ${service.status?.observedGeneration} does not ` + + `match spec generation ${service.metadata.generation}`, + ); + return false; + } + const readyCondition = service.status?.conditions?.find((condition) => { + return condition.type === "Ready"; + }); + + if (readyCondition?.status === "Unknown") { + logger.debug( + `Waiting for service ${service.metadata.name} to be ready. ` + + `Status is ${JSON.stringify(service.status?.conditions)}`, + ); + return false; + } else if (readyCondition?.status === "True") { + return true; + } + logger.debug( + `Service ${service.metadata.name} has unexpected ready status ${JSON.stringify( + readyCondition, + )}. It may have failed rollout.`, + ); + throw new FirebaseError( + `Unexpected Status ${readyCondition?.status} for service ${service.metadata.name}`, + ); +} + +/** + * Replaces a service spec. Prefer updateService to block on replication. + */ export async function replaceService(name: string, service: Service): Promise { try { const response = await client.put(name, service); return response.body; - } catch (err) { - throw new FirebaseError(`Failed to update Run service ${name}`, { + } catch (err: any) { + throw new FirebaseError(`Failed to replace Run service ${name}`, { original: err, + status: err?.context?.response?.statusCode, }); } } @@ -143,7 +234,7 @@ export async function replaceService(name: string, service: Service): Promise { // Cloud Run has an atypical REST binding for SetIamPolicy. Instead of making the body a policy and // the update mask a query parameter (e.g. Cloud Functions v1) the request body is the literal @@ -157,21 +248,25 @@ export async function setIamPolicy( policy, updateMask: proto.fieldMasks(policy).join(","), }); - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to set the IAM Policy on the Service ${name}`, { original: err, + status: err?.context?.response?.statusCode, }); } } +/** + * Gets IAM policy for a service. + */ export async function getIamPolicy( serviceName: string, - httpClient: Client = client + httpClient: Client = client, ): Promise { try { const response = await httpClient.get(`${serviceName}:getIamPolicy`); return response.body; - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to get the IAM Policy on the Service ${serviceName}`, { original: err, }); @@ -183,16 +278,15 @@ export async function getIamPolicy( * @param projectId id of the project * @param serviceName cloud run service * @param invoker an array of invoker strings - * * @throws {@link FirebaseError} on an empty invoker, when the IAM Polciy fails to be grabbed or set */ export async function setInvokerCreate( projectId: string, serviceName: string, invoker: string[], - httpClient: Client = client // for unit testing + httpClient: Client = client, // for unit testing ) { - if (invoker.length == 0) { + if (invoker.length === 0) { throw new FirebaseError("Invoker cannot be an empty array"); } const invokerMembers = proto.getInvokerMembers(invoker, projectId); @@ -213,23 +307,22 @@ export async function setInvokerCreate( * @param projectId id of the project * @param serviceName cloud run service * @param invoker an array of invoker strings - * * @throws {@link FirebaseError} on an empty invoker, when the IAM Polciy fails to be grabbed or set */ export async function setInvokerUpdate( projectId: string, serviceName: string, invoker: string[], - httpClient: Client = client // for unit testing + httpClient: Client = client, // for unit testing ) { - if (invoker.length == 0) { + if (invoker.length === 0) { throw new FirebaseError("Invoker cannot be an empty array"); } const invokerMembers = proto.getInvokerMembers(invoker, projectId); const invokerRole = "roles/run.invoker"; const currentPolicy = await getIamPolicy(serviceName, httpClient); const currentInvokerBinding = currentPolicy.bindings?.find( - (binding) => binding.role === invokerRole + (binding) => binding.role === invokerRole, ); if ( currentInvokerBinding && diff --git a/src/gcp/runtimeconfig.js b/src/gcp/runtimeconfig.js deleted file mode 100644 index c3c1a538d0e..00000000000 --- a/src/gcp/runtimeconfig.js +++ /dev/null @@ -1,166 +0,0 @@ -"use strict"; - -var api = require("../api"); - -var utils = require("../utils"); -const { logger } = require("../logger"); -var _ = require("lodash"); - -var API_VERSION = "v1beta1"; - -function _listConfigs(projectId) { - return api - .request("GET", utils.endpoint([API_VERSION, "projects", projectId, "configs"]), { - auth: true, - origin: api.runtimeconfigOrigin, - retryCodes: [500, 503], - }) - .then(function (resp) { - return Promise.resolve(resp.body.configs); - }); -} - -function _createConfig(projectId, configId) { - var path = _.join(["projects", projectId, "configs"], "/"); - var endpoint = utils.endpoint([API_VERSION, path]); - return api - .request("POST", endpoint, { - auth: true, - origin: api.runtimeconfigOrigin, - data: { - name: path + "/" + configId, - }, - retryCodes: [500, 503], - }) - .catch(function (err) { - if (_.get(err, "context.response.statusCode") === 409) { - // Config has already been created as part of a parallel operation during firebase functions:config:set - return Promise.resolve(); - } - return Promise.reject(err); - }); -} - -function _deleteConfig(projectId, configId) { - return api - .request("DELETE", utils.endpoint([API_VERSION, "projects", projectId, "configs", configId]), { - auth: true, - origin: api.runtimeconfigOrigin, - retryCodes: [500, 503], - }) - .catch(function (err) { - if (_.get(err, "context.response.statusCode") === 404) { - logger.debug("Config already deleted."); - return Promise.resolve(); - } - return Promise.reject(err); - }); -} - -function _listVariables(configPath) { - return api - .request("GET", utils.endpoint([API_VERSION, configPath, "variables"]), { - auth: true, - origin: api.runtimeconfigOrigin, - retryCodes: [500, 503], - }) - .then(function (resp) { - return Promise.resolve(resp.body.variables); - }); -} - -function _getVariable(varPath) { - return api - .request("GET", utils.endpoint([API_VERSION, varPath]), { - auth: true, - origin: api.runtimeconfigOrigin, - retryCodes: [500, 503], - }) - .then(function (resp) { - return Promise.resolve(resp.body); - }); -} - -function _createVariable(projectId, configId, varId, value) { - var path = _.join(["projects", projectId, "configs", configId, "variables"], "/"); - var endpoint = utils.endpoint([API_VERSION, path]); - return api - .request("POST", endpoint, { - auth: true, - origin: api.runtimeconfigOrigin, - data: { - name: path + "/" + varId, - text: value, - }, - retryCodes: [500, 503], - }) - .catch(function (err) { - if (_.get(err, "context.response.statusCode") === 404) { - // parent config doesn't exist yet - return _createConfig(projectId, configId).then(function () { - return _createVariable(projectId, configId, varId, value); - }); - } - return Promise.reject(err); - }); -} - -function _updateVariable(projectId, configId, varId, value) { - var path = _.join(["projects", projectId, "configs", configId, "variables", varId], "/"); - var endpoint = utils.endpoint([API_VERSION, path]); - return api.request("PUT", endpoint, { - auth: true, - origin: api.runtimeconfigOrigin, - data: { - name: path, - text: value, - }, - retryCodes: [500, 503], - }); -} -function _setVariable(projectId, configId, varId, value) { - var path = _.join(["projects", projectId, "configs", configId, "variables", varId], "/"); - return _getVariable(path) - .then(function () { - return _updateVariable(projectId, configId, varId, value); - }) - .catch(function (err) { - if (_.get(err, "context.response.statusCode") === 404) { - return _createVariable(projectId, configId, varId, value); - } - return Promise.reject(err); - }); -} - -function _deleteVariable(projectId, configId, varId) { - var endpoint = - utils.endpoint([API_VERSION, "projects", projectId, "configs", configId, "variables", varId]) + - "?recursive=true"; - return api - .request("DELETE", endpoint, { - auth: true, - origin: api.runtimeconfigOrigin, - retryCodes: [500, 503], - }) - .catch(function (err) { - if (_.get(err, "context.response.statusCode") === 404) { - logger.debug("Variable already deleted."); - return Promise.resolve(); - } - return Promise.reject(err); - }); -} - -module.exports = { - configs: { - list: _listConfigs, - create: _createConfig, - delete: _deleteConfig, - }, - variables: { - list: _listVariables, - get: _getVariable, - set: _setVariable, - delete: _deleteVariable, - }, -}; diff --git a/src/gcp/runtimeconfig.ts b/src/gcp/runtimeconfig.ts new file mode 100644 index 00000000000..331ab20adb0 --- /dev/null +++ b/src/gcp/runtimeconfig.ts @@ -0,0 +1,160 @@ +import * as _ from "lodash"; + +import { runtimeconfigOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; + +const API_VERSION = "v1beta1"; +const apiClient = new Client({ urlPrefix: runtimeconfigOrigin(), apiVersion: API_VERSION }); + +function listConfigs(projectId: string): Promise { + return apiClient + .get<{ configs?: unknown[] }>(`/projects/${projectId}/configs`, { + retryCodes: [500, 503], + }) + .then((resp) => resp.body.configs); +} + +function createConfig(projectId: string, configId: string): Promise { + const path = ["projects", projectId, "configs"].join("/"); + return apiClient + .post( + `/projects/${projectId}/configs`, + { + name: path + "/" + configId, + }, + { + retryCodes: [500, 503], + }, + ) + .catch((err) => { + if (_.get(err, "context.response.statusCode") === 409) { + // Config has already been created as part of a parallel operation during firebase functions:config:set + return Promise.resolve(); + } + return Promise.reject(err); + }); +} + +function deleteConfig(projectId: string, configId: string): Promise { + return apiClient + .delete(`/projects/${projectId}/configs/${configId}`, { + retryCodes: [500, 503], + }) + .catch((err) => { + if (_.get(err, "context.response.statusCode") === 404) { + logger.debug("Config already deleted."); + return Promise.resolve(); + } + throw err; + }); +} + +function listVariables(configPath: string): Promise<{ name: string }[]> { + return apiClient + .get<{ variables: any }>(`${configPath}/variables`, { + retryCodes: [500, 503], + }) + .then((resp) => { + return Promise.resolve(resp.body.variables || []); + }); +} + +function getVariable(varPath: string): Promise { + return apiClient + .get(varPath, { + retryCodes: [500, 503], + }) + .then((resp) => { + return Promise.resolve(resp.body); + }); +} + +function createVariable( + projectId: string, + configId: string, + varId: string, + value: any, +): Promise { + const path = `/projects/${projectId}/configs/${configId}/variables`; + return apiClient + .post( + path, + { + name: `${path}/${varId}`, + text: value, + }, + { + retryCodes: [500, 503], + }, + ) + .catch((err) => { + if (_.get(err, "context.response.statusCode") === 404) { + // parent config doesn't exist yet + return createConfig(projectId, configId).then(() => { + return createVariable(projectId, configId, varId, value); + }); + } + return Promise.reject(err); + }); +} + +function updateVariable( + projectId: string, + configId: string, + varId: string, + value: any, +): Promise { + const path = `/projects/${projectId}/configs/${configId}/variables/${varId}`; + return apiClient.put( + path, + { + name: path, + text: value, + }, + { + retryCodes: [500, 503], + }, + ); +} + +function setVariable(projectId: string, configId: string, varId: string, value: any): Promise { + const path = ["projects", projectId, "configs", configId, "variables", varId].join("/"); + return getVariable(path) + .then(() => { + return updateVariable(projectId, configId, varId, value); + }) + .catch((err) => { + if (_.get(err, "context.response.statusCode") === 404) { + return createVariable(projectId, configId, varId, value); + } + return Promise.reject(err); + }); +} + +function deleteVariable(projectId: string, configId: string, varId: string): Promise { + return apiClient + .delete(`/projects/${projectId}/configs/${configId}/variables/${varId}`, { + retryCodes: [500, 503], + queryParams: { recursive: "true" }, + }) + .catch((err) => { + if (_.get(err, "context.response.statusCode") === 404) { + logger.debug("Variable already deleted."); + return Promise.resolve(); + } + return Promise.reject(err); + }); +} + +export const configs = { + list: listConfigs, + create: createConfig, + delete: deleteConfig, +}; +export const variables = { + list: listVariables, + get: getVariable, + set: setVariable, + delete: deleteVariable, +}; diff --git a/src/gcp/secretManager.spec.ts b/src/gcp/secretManager.spec.ts new file mode 100644 index 00000000000..542084df53e --- /dev/null +++ b/src/gcp/secretManager.spec.ts @@ -0,0 +1,135 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as iam from "./iam"; +import * as secretManager from "./secretManager"; +import { FirebaseError } from "../error"; +import { ensureServiceAgentRole } from "./secretManager"; + +describe("secretManager", () => { + describe("parseSecretResourceName", () => { + it("parses valid secret resource name", () => { + expect( + secretManager.parseSecretResourceName("projects/my-project/secrets/my-secret"), + ).to.deep.equal({ projectId: "my-project", name: "my-secret", labels: {}, replication: {} }); + }); + + it("throws given invalid resource name", () => { + expect(() => secretManager.parseSecretResourceName("foo/bar")).to.throw(FirebaseError); + }); + + it("throws given incomplete resource name", () => { + expect(() => secretManager.parseSecretResourceName("projects/my-project")).to.throw( + FirebaseError, + ); + }); + + it("parse secret version resource name", () => { + expect( + secretManager.parseSecretResourceName("projects/my-project/secrets/my-secret/versions/8"), + ).to.deep.equal({ projectId: "my-project", name: "my-secret", labels: {}, replication: {} }); + }); + }); + + describe("parseSecretVersionResourceName", () => { + it("parses valid secret resource name", () => { + expect( + secretManager.parseSecretVersionResourceName( + "projects/my-project/secrets/my-secret/versions/7", + ), + ).to.deep.equal({ + secret: { projectId: "my-project", name: "my-secret", labels: {}, replication: {} }, + versionId: "7", + createTime: "", + }); + }); + + it("throws given invalid resource name", () => { + expect(() => secretManager.parseSecretVersionResourceName("foo/bar")).to.throw(FirebaseError); + }); + + it("throws given incomplete resource name", () => { + expect(() => secretManager.parseSecretVersionResourceName("projects/my-project")).to.throw( + FirebaseError, + ); + }); + + it("throws given secret resource name", () => { + expect(() => + secretManager.parseSecretVersionResourceName("projects/my-project/secrets/my-secret"), + ).to.throw(FirebaseError); + }); + }); + + describe("ensureServiceAgentRole", () => { + const projectId = "my-project"; + const secret = { projectId, name: "my-secret" }; + const role = "test-role"; + + let getIamPolicyStub: sinon.SinonStub; + let setIamPolicyStub: sinon.SinonStub; + + beforeEach(() => { + getIamPolicyStub = sinon.stub(secretManager, "getIamPolicy").rejects("Unexpected call"); + setIamPolicyStub = sinon.stub(secretManager, "setIamPolicy").rejects("Unexpected call"); + }); + + afterEach(() => { + getIamPolicyStub.restore(); + setIamPolicyStub.restore(); + }); + + function setupStubs(existing: iam.Binding[], expected?: iam.Binding[]) { + getIamPolicyStub.withArgs(secret).resolves({ bindings: existing }); + if (expected) { + setIamPolicyStub.withArgs(secret, expected).resolves({ body: { bindings: expected } }); + } + } + + it("adds new binding for each member", async () => { + const existing: iam.Binding[] = []; + const expected: iam.Binding[] = [ + { role, members: ["serviceAccount:1@foobar.com", "serviceAccount:2@foobar.com"] }, + ]; + + setupStubs(existing, expected); + await ensureServiceAgentRole(secret, ["1@foobar.com", "2@foobar.com"], role); + }); + + it("adds bindings only for missing members", async () => { + const existing: iam.Binding[] = [{ role, members: ["serviceAccount:1@foobar.com"] }]; + const expected: iam.Binding[] = [ + { role, members: ["serviceAccount:1@foobar.com", "serviceAccount:2@foobar.com"] }, + ]; + + setupStubs(existing, expected); + await ensureServiceAgentRole(secret, ["1@foobar.com", "2@foobar.com"], role); + }); + + it("keeps bindings that already exists", async () => { + const existing: iam.Binding[] = [ + { role: "another-role", members: ["serviceAccount:3@foobar.com"] }, + ]; + const expected: iam.Binding[] = [ + { + role: "another-role", + members: ["serviceAccount:3@foobar.com"], + }, + { + role, + members: ["serviceAccount:1@foobar.com", "serviceAccount:2@foobar.com"], + }, + ]; + + setupStubs(existing, expected); + await ensureServiceAgentRole(secret, ["1@foobar.com", "2@foobar.com"], role); + }); + + it("does nothing if the binding already exists", async () => { + const existing: iam.Binding[] = [{ role, members: ["serviceAccount:1@foobar.com"] }]; + + setupStubs(existing); + await ensureServiceAgentRole(secret, ["1@foobar.com"], role); + }); + }); +}); diff --git a/src/gcp/secretManager.ts b/src/gcp/secretManager.ts index 424b8a10a3b..32b419ce471 100644 --- a/src/gcp/secretManager.ts +++ b/src/gcp/secretManager.ts @@ -1,5 +1,24 @@ +import * as iam from "./iam"; + import { logLabeledSuccess } from "../utils"; -import * as api from "../api"; +import { FirebaseError } from "../error"; +import { Client } from "../apiv2"; +import { secretManagerOrigin } from "../api"; +import * as ensureApiEnabled from "../ensureApiEnabled"; +import { needProjectId } from "../projectUtils"; + +// Matches projects/{PROJECT}/secrets/{SECRET} +const SECRET_NAME_REGEX = new RegExp( + "projects\\/" + + "(?(?:\\d+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/" + + "secrets\\/" + + "(?[A-Za-z\\d\\-_]+)", +); + +// Matches projects/{PROJECT}/secrets/{SECRET}/versions/{latest|VERSION} +const SECRET_VERSION_NAME_REGEX = new RegExp( + SECRET_NAME_REGEX.source + "\\/versions\\/" + "(?latest|[0-9]+)", +); export const secretManagerConsoleUri = (projectId: string) => `https://console.cloud.google.com/security/secret-manager?project=${projectId}`; @@ -8,53 +27,226 @@ export interface Secret { name: string; // This is either projectID or number projectId: string; - labels?: Record; + labels: Record; + replication: Replication; +} + +export interface WireSecret { + name: string; + labels: Record; + replication: Replication; +} + +type SecretVersionState = "STATE_UNSPECIFIED" | "ENABLED" | "DISABLED" | "DESTROYED"; + +export interface Replication { + automatic?: {}; + userManaged?: { + replicas: Array<{ + location: string; + customerManagedEncryption?: { + kmsKeyName: string; + }; + }>; + }; } export interface SecretVersion { secret: Secret; versionId: string; + + // Output-only fields + readonly state?: SecretVersionState; + readonly createTime?: string; } -export async function listSecrets(projectId: string): Promise { - const listRes = await api.request("GET", `/v1beta1/projects/${projectId}/secrets`, { - auth: true, - origin: api.secretManagerOrigin, - }); - return listRes.body.secrets.map((s: any) => parseSecretResourceName(s.name)); +interface CreateSecretRequest { + name: string; + replication: Replication; + labels: Record; } +interface AddVersionRequest { + payload: { data: string }; +} + +interface SecretVersionResponse { + name: string; + state: SecretVersionState; + createTime: string; +} + +interface AccessSecretVersionResponse { + name: string; + payload: { + data: string; + }; +} + +const API_VERSION = "v1"; + +const client = new Client({ urlPrefix: secretManagerOrigin(), apiVersion: API_VERSION }); + +/** + * Returns secret resource of given name in the project. + */ export async function getSecret(projectId: string, name: string): Promise { - const getRes = await api.request("GET", `/v1beta1/projects/${projectId}/secrets/${name}`, { - auth: true, - origin: api.secretManagerOrigin, - }); + const getRes = await client.get(`projects/${projectId}/secrets/${name}`); const secret = parseSecretResourceName(getRes.body.name); secret.labels = getRes.body.labels ?? {}; + secret.replication = getRes.body.replication ?? {}; return secret; } -export async function getSecretVersion( +/** + * Lists all secret resources associated with a project. + */ +export async function listSecrets(projectId: string, filter?: string): Promise { + type Response = { secrets: WireSecret[]; nextPageToken?: string }; + const secrets: Secret[] = []; + const path = `projects/${projectId}/secrets`; + const baseOpts = filter ? { queryParams: { filter } } : {}; + + let pageToken = ""; + while (true) { + const opts = + pageToken === "" + ? baseOpts + : { ...baseOpts, queryParams: { ...baseOpts?.queryParams, pageToken } }; + const res = await client.get(path, opts); + + for (const s of res.body.secrets || []) { + secrets.push({ + ...parseSecretResourceName(s.name), + labels: s.labels ?? {}, + replication: s.replication ?? {}, + }); + } + + if (!res.body.nextPageToken) { + break; + } + pageToken = res.body.nextPageToken; + } + return secrets; +} + +/** + * Retrieves a specific Secret and SecretVersion from CSM, if available. + */ +export async function getSecretMetadata( + projectId: string, + secretName: string, + version: string, +): Promise<{ + secret?: Secret; + secretVersion?: SecretVersion; +}> { + const secretInfo: any = {}; + try { + secretInfo.secret = await getSecret(projectId, secretName); + secretInfo.secretVersion = await getSecretVersion(projectId, secretName, version); + } catch (err: any) { + // Throw anything other than the expected 404 errors. + if (err.status !== 404) { + throw err; + } + } + return secretInfo; +} + +/** + * List all secret versions associated with a secret. + */ +export async function listSecretVersions( projectId: string, name: string, - version: string -): Promise { - const getRes = await api.request( - "GET", - `/v1beta1/projects/${projectId}/secrets/${name}/versions/${version}`, - { - auth: true, - origin: api.secretManagerOrigin, + filter?: string, +): Promise> { + type Response = { versions: SecretVersionResponse[]; nextPageToken?: string }; + const secrets: Required = []; + const path = `projects/${projectId}/secrets/${name}/versions`; + const baseOpts = filter ? { queryParams: { filter } } : {}; + + let pageToken = ""; + while (true) { + const opts = + pageToken === "" + ? baseOpts + : { ...baseOpts, queryParams: { ...baseOpts?.queryParams, pageToken } }; + const res = await client.get(path, opts); + + for (const s of res.body.versions || []) { + secrets.push({ + ...parseSecretVersionResourceName(s.name), + state: s.state, + createTime: s.createTime, + }); } + + if (!res.body.nextPageToken) { + break; + } + pageToken = res.body.nextPageToken; + } + return secrets; +} + +/** + * Returns secret version resource of given name and version in the project. + */ +export async function getSecretVersion( + projectId: string, + name: string, + version: string, +): Promise> { + const getRes = await client.get( + `projects/${projectId}/secrets/${name}/versions/${version}`, + ); + return { + ...parseSecretVersionResourceName(getRes.body.name), + state: getRes.body.state, + createTime: getRes.body.createTime, + }; +} + +/** + * Access secret value of a given secret version. + */ +export async function accessSecretVersion( + projectId: string, + name: string, + version: string, +): Promise { + const res = await client.get( + `projects/${projectId}/secrets/${name}/versions/${version}:access`, ); - return parseSecretVersionResourceName(getRes.body.name); + return Buffer.from(res.body.payload.data, "base64").toString(); } +/** + * Change state of secret version to destroyed. + */ +export async function destroySecretVersion( + projectId: string, + name: string, + version: string, +): Promise { + if (version === "latest") { + const sv = await getSecretVersion(projectId, name, "latest"); + version = sv.versionId; + } + await client.post(`projects/${projectId}/secrets/${name}/versions/${version}:destroy`); +} + +/** + * Returns true if secret resource of given name exists on the project. + */ export async function secretExists(projectId: string, name: string): Promise { try { await getSecret(projectId, name); return true; - } catch (err) { + } catch (err: any) { if (err.status === 404) { return false; } @@ -62,124 +254,242 @@ export async function secretExists(projectId: string, name: string): Promise + labels: Record, + location?: string, ): Promise { - const createRes = await api.request( - "POST", - `/v1beta1/projects/${projectId}/secrets?secretId=${name}`, - { - auth: true, - origin: api.secretManagerOrigin, - data: { - replication: { - automatic: {}, - }, - labels, + let replication: CreateSecretRequest["replication"]; + if (location) { + replication = { + userManaged: { + replicas: [ + { + location, + }, + ], }, - } + }; + } else { + replication = { automatic: {} }; + } + + const createRes = await client.post( + `projects/${projectId}/secrets`, + { + name, + replication, + labels, + }, + { queryParams: { secretId: name } }, ); - return parseSecretResourceName(createRes.body.name); + return { + ...parseSecretResourceName(createRes.body.name), + labels, + replication, + }; } -export async function addVersion(secret: Secret, payloadData: string): Promise { - const res = await api.request( - "POST", - `/v1beta1/projects/${secret.projectId}/secrets/${secret.name}:addVersion`, +/** + * Update metadata associated with a secret. + */ +export async function patchSecret( + projectId: string, + name: string, + labels: Record, +): Promise { + const fullName = `projects/${projectId}/secrets/${name}`; + const res = await client.patch, WireSecret>( + fullName, + { name: fullName, labels }, + { queryParams: { updateMask: "labels" } }, // Only allow patching labels for now. + ); + return { + ...parseSecretResourceName(res.body.name), + labels: res.body.labels, + replication: res.body.replication, + }; +} + +/** + * Delete secret resource. + */ +export async function deleteSecret(projectId: string, name: string): Promise { + const path = `projects/${projectId}/secrets/${name}`; + await client.delete(path); +} + +/** + * Add new version the payload as value on the given secret. + */ +export async function addVersion( + projectId: string, + name: string, + payloadData: string, +): Promise> { + const res = await client.post( + `projects/${projectId}/secrets/${name}:addVersion`, { - auth: true, - origin: api.secretManagerOrigin, - data: { - payload: { - data: Buffer.from(payloadData).toString("base64"), - }, + payload: { + data: Buffer.from(payloadData).toString("base64"), }, - } + }, ); - const nameTokens = res.body.name.split("/"); return { - secret: { - projectId: nameTokens[1], - name: nameTokens[3], - }, - versionId: nameTokens[5], + ...parseSecretVersionResourceName(res.body.name), + state: res.body.state, + createTime: "", }; } -export async function grantServiceAgentRole( - secret: Secret, - serviceAccountEmail: string, - role: string +/** + * Returns IAM policy of a secret resource. + */ +export async function getIamPolicy( + secret: Pick, +): Promise { + const res = await client.get( + `projects/${secret.projectId}/secrets/${secret.name}:getIamPolicy`, + ); + return res.body; +} + +/** + * Sets IAM policy on a secret resource. + */ +export async function setIamPolicy( + secret: Pick, + bindings: iam.Binding[], ): Promise { - const getPolicyRes = await api.request( - "GET", - `/v1beta1/projects/${secret.projectId}/secrets/${secret.name}:getIamPolicy`, + await client.post<{ policy: Partial; updateMask: string }, iam.Policy>( + `projects/${secret.projectId}/secrets/${secret.name}:setIamPolicy`, { - auth: true, - origin: api.secretManagerOrigin, - } + policy: { + bindings, + }, + updateMask: "bindings", + }, ); +} - const bindings = getPolicyRes.body.bindings || []; - if ( - bindings.find( - (b: any) => - b.role == role && - b.members.find((m: string) => m == `serviceAccount:${serviceAccountEmail}`) - ) - ) { - // binding already exists, short-circuit. - return; +/** + * Ensure that given service agents have the given IAM role on the secret resource. + */ +export async function ensureServiceAgentRole( + secret: Pick, + serviceAccountEmails: string[], + role: string, +): Promise { + const policy = await module.exports.getIamPolicy(secret); + const bindings: iam.Binding[] = policy.bindings || []; + let binding = bindings.find((b) => b.role === role); + if (!binding) { + binding = { role, members: [] }; + bindings.push(binding); } - bindings.push({ - role: role, - members: [`serviceAccount:${serviceAccountEmail}`], - }); - await api.request( - "POST", - `/v1beta1/projects/${secret.projectId}/secrets/${secret.name}:setIamPolicy`, - { - auth: true, - origin: api.secretManagerOrigin, - data: { - policy: { - bindings, - }, - updateMask: { - paths: "bindings", - }, - }, + + let shouldShortCircuit = true; + for (const serviceAccount of serviceAccountEmails) { + if (!binding.members.find((m) => m === `serviceAccount:${serviceAccount}`)) { + binding.members.push(`serviceAccount:${serviceAccount}`); + shouldShortCircuit = false; } - ); + } + + if (shouldShortCircuit) return; + + await module.exports.setIamPolicy(secret, bindings); + // SecretManager would like us to _always_ inform users when we grant access to one of their secrets. // As a safeguard against forgetting to do so, we log it here. logLabeledSuccess( - "SecretManager", - `Granted ${role} on projects/${secret.projectId}/secrets/${secret.name} to ${serviceAccountEmail}` + "secretmanager", + `Granted ${role} on projects/${secret.projectId}/secrets/${ + secret.name + } to ${serviceAccountEmails.join(", ")}`, + ); +} + +export const FIREBASE_MANAGED = "firebase-managed"; + +/** + * Returns true if secret is managed by Cloud Functions for Firebase. + * This used to be firebase-managed: true, but was later changed to firebase-managed: functions to + * improve readability. + */ +export function isFunctionsManaged(secret: Secret): boolean { + return ( + secret.labels[FIREBASE_MANAGED] === "true" || secret.labels[FIREBASE_MANAGED] === "functions" ); } + +/** + * Returns true if secret is managed by Firebase App Hosting. + */ +export function isAppHostingManaged(secret: Secret): boolean { + return secret.labels[FIREBASE_MANAGED] === "apphosting"; +} + +/** + * Utility used in the "before" command annotation to enable the API. + */ + +export function ensureApi(options: any): Promise { + const projectId = needProjectId(options); + return ensureApiEnabled.ensure(projectId, secretManagerOrigin(), "secretmanager", true); +} +/** + * Return labels to mark secret as managed by Firebase. + * @internal + */ + +export function labels(product: "functions" | "apphosting" = "functions"): Record { + return { [FIREBASE_MANAGED]: product }; +} diff --git a/src/gcp/serviceusage.spec.ts b/src/gcp/serviceusage.spec.ts new file mode 100644 index 00000000000..a7360e515f0 --- /dev/null +++ b/src/gcp/serviceusage.spec.ts @@ -0,0 +1,31 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as serviceUsage from "./serviceusage"; +import * as poller from "../operation-poller"; + +describe("serviceusage", () => { + let postStub: sinon.SinonStub; + let pollerStub: sinon.SinonStub; + + const projectNumber = "projectNumber"; + const service = "service"; + const prefix = "prefix"; + + beforeEach(() => { + postStub = sinon.stub(serviceUsage.apiClient, "post").throws("unexpected post call"); + pollerStub = sinon.stub(poller, "pollOperation").throws("unexpected pollOperation call"); + }); + + afterEach(() => { + postStub.restore(); + pollerStub.restore(); + }); + + describe("generateServiceIdentityAndPoll", () => { + it("does not poll if generateServiceIdentity responds with a completed operation", async () => { + postStub.onFirstCall().resolves({ body: { done: true } }); + await serviceUsage.generateServiceIdentityAndPoll(projectNumber, service, prefix); + expect(pollerStub).to.not.be.called; + }); + }); +}); diff --git a/src/gcp/serviceusage.ts b/src/gcp/serviceusage.ts new file mode 100644 index 00000000000..f572c1bf718 --- /dev/null +++ b/src/gcp/serviceusage.ts @@ -0,0 +1,69 @@ +import { bold } from "colorette"; +import { serviceUsageOrigin } from "../api"; +import { Client } from "../apiv2"; +import { FirebaseError } from "../error"; +import * as utils from "../utils"; +import * as poller from "../operation-poller"; +import { LongRunningOperation } from "../operation-poller"; + +const API_VERSION = "v1beta1"; +const SERVICE_USAGE_ORIGIN = serviceUsageOrigin(); + +export const apiClient = new Client({ + urlPrefix: SERVICE_USAGE_ORIGIN, + apiVersion: API_VERSION, +}); + +const serviceUsagePollerOptions: Omit = { + apiOrigin: SERVICE_USAGE_ORIGIN, + apiVersion: API_VERSION, +}; + +/** + * Generate the service account for the service. Note: not every service uses the endpoint. + * @param projectNumber gcp project number + * @param service the service api (ex~ pubsub.googleapis.com) + * @return Promise + */ +export async function generateServiceIdentity( + projectNumber: string, + service: string, + prefix: string, +): Promise> { + utils.logLabeledBullet(prefix, `generating the service identity for ${bold(service)}...`); + try { + const res = await apiClient.post( + `projects/${projectNumber}/services/${service}:generateServiceIdentity`, + ); + return res.body as LongRunningOperation; + } catch (err: unknown) { + throw new FirebaseError(`Error generating the service identity for ${service}.`, { + original: err as Error, + }); + } +} + +/** + * Calls GenerateServiceIdentity and polls till the operation is complete. + */ +export async function generateServiceIdentityAndPoll( + projectNumber: string, + service: string, + prefix: string, +): Promise { + const op = await generateServiceIdentity(projectNumber, service, prefix); + /** + * Note: generateServiceIdenity seems to return a DONE operation with an + * operation name of "finished.DONE_OPERATION" and querying the operation + * returns a 400 error. As a workaround we check if the operation is DONE + * before beginning to poll. + */ + if (op.done) { + return; + } + + await poller.pollOperation({ + ...serviceUsagePollerOptions, + operationResourceName: op.name, + }); +} diff --git a/src/gcp/storage.ts b/src/gcp/storage.ts index 14c8042e706..750a591b4c0 100644 --- a/src/gcp/storage.ts +++ b/src/gcp/storage.ts @@ -1,8 +1,12 @@ +import { Readable } from "stream"; import * as path from "path"; +import * as clc from "colorette"; -import * as api from "../api"; -import { logger } from "../logger"; +import { firebaseStorageOrigin, storageOrigin } from "../api"; +import { Client } from "../apiv2"; import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { ensure } from "../ensureApiEnabled"; /** Bucket Interface */ interface BucketResponse { @@ -36,7 +40,7 @@ interface BucketResponse { team: string; }; etag: string; - } + }, ]; defaultObjectAcl: [ { @@ -51,7 +55,7 @@ interface BucketResponse { team: string; }; etag: string; - } + }, ]; iamConfiguration: { publicAccessPrevention: string; @@ -87,7 +91,7 @@ interface BucketResponse { method: [string]; responseHeader: [string]; maxAgeSeconds: number; - } + }, ]; lifecycle: { rule: [ @@ -107,7 +111,7 @@ interface BucketResponse { noncurrentTimeBefore: string; numNewerVersions: number; }; - } + }, ]; }; labels: { @@ -120,29 +124,56 @@ interface BucketResponse { etag: string; } +interface ListBucketsResponse { + kind: string; + nextPageToken: string; + items: [ + { + name: string; + }, + ]; +} + +interface GetDefaultBucketResponse { + name: string; + location: string; + bucket: { + name: string; + }; +} + /** Response type for obtaining the storage service agent */ interface StorageServiceAccountResponse { email_address: string; kind: string; } -export async function getDefaultBucket(projectId?: string): Promise { +export async function getDefaultBucket(projectId: string): Promise { + await ensure(projectId, firebaseStorageOrigin(), "storage", false); try { - const resp = await api.request("GET", "/v1/apps/" + projectId, { - auth: true, - origin: api.appengineOrigin, + const localAPIClient = new Client({ + urlPrefix: firebaseStorageOrigin(), + apiVersion: "v1alpha", }); - if (resp.body.defaultBucket === "undefined") { + const response = await localAPIClient.get( + `/projects/${projectId}/defaultBucket`, + ); + if (!response.body?.bucket.name) { logger.debug("Default storage bucket is undefined."); throw new FirebaseError( - "Your project is being set up. Please wait a minute before deploying again." + "Your project is being set up. Please wait a minute before deploying again.", ); } - return resp.body.defaultBucket; - } catch (err) { - logger.info( - "\n\nThere was an issue deploying your functions. Verify that your project has a Google App Engine instance setup at https://console.cloud.google.com/appengine and try again. If this issue persists, please contact support." - ); + return response.body.bucket.name.split("/").pop()!; + } catch (err: any) { + if (err?.status === 404) { + throw new FirebaseError( + `Firebase Storage has not been set up on project '${clc.bold( + projectId, + )}'. Go to https://console.firebase.google.com/project/${projectId}/storage and click 'Get Started' to set up Firebase Storage.`, + ); + } + logger.info("\n\nUnexpected error when fetching default storage bucket."); throw err; } } @@ -150,52 +181,56 @@ export async function getDefaultBucket(projectId?: string): Promise { export async function upload( source: any, uploadUrl: string, - extraHeaders?: Record + extraHeaders?: Record, + ignoreQuotaProject?: boolean, ): Promise { const url = new URL(uploadUrl); - const result = await api.request("PUT", url.pathname + url.search, { - data: source.stream, + const localAPIClient = new Client({ urlPrefix: url.origin, auth: false }); + const res = await localAPIClient.request({ + method: "PUT", + path: url.pathname, + queryParams: url.searchParams, + responseType: "xml", headers: { - "Content-Type": "application/zip", + "content-type": "application/zip", ...extraHeaders, }, - json: false, - origin: url.origin, - logOptions: { skipRequestBody: true }, + body: source.stream, + skipLog: { resBody: true }, + ignoreQuotaProject, }); - return { - generation: result.response.headers["x-goog-generation"], + generation: res.response.headers.get("x-goog-generation"), }; } /** * Uploads a zip file to the specified bucket using the firebasestorage api. - * @param {!Object} source a zip file to upload. Must contain: - * - `file` {string}: file name - * - `stream` {Stream}: read stream of the archive - * @param {string} bucketName a bucket to upload to */ -export async function uploadObject(source: any, bucketName: string): Promise { +export async function uploadObject( + /** Source with file (name) to upload, and stream of file. */ + source: { file: string; stream: Readable }, + /** Bucket to upload to. */ + bucketName: string, +): Promise<{ bucket: string; object: string; generation: string | null }> { if (path.extname(source.file) !== ".zip") { throw new FirebaseError(`Expected a file name ending in .zip, got ${source.file}`); } + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); const location = `/${bucketName}/${path.basename(source.file)}`; - const result = await api.request("PUT", location, { - auth: true, - data: source.stream, + const res = await localAPIClient.request({ + method: "PUT", + path: location, headers: { "Content-Type": "application/zip", "x-goog-content-length-range": "0,123289600", }, - json: false, - origin: api.storageOrigin, - logOptions: { skipRequestBody: true }, + body: source.stream, }); return { bucket: bucketName, object: path.basename(source.file), - generation: result.response.headers["x-goog-generation"], + generation: res.response.headers.get("x-goog-generation"), }; } @@ -204,26 +239,22 @@ export async function uploadObject(source: any, bucketName: string): Promise/o/" */ export function deleteObject(location: string): Promise { - return api.request("DELETE", location, { - auth: true, - origin: api.storageOrigin, - }); + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + return localAPIClient.delete(location); } /** * Gets a storage bucket from GCP. * Ref: https://cloud.google.com/storage/docs/json_api/v1/buckets/get * @param {string} bucketName name of the storage bucket - * @returns a bucket resource object + * @return a bucket resource object */ export async function getBucket(bucketName: string): Promise { try { - const result = await api.request("GET", `/storage/v1/b/${bucketName}`, { - auth: true, - origin: api.storageOrigin, - }); + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + const result = await localAPIClient.get(`/storage/v1/b/${bucketName}`); return result.body; - } catch (err) { + } catch (err: any) { logger.debug(err); throw new FirebaseError("Failed to obtain the storage bucket", { original: err, @@ -231,10 +262,30 @@ export async function getBucket(bucketName: string): Promise { } } +/** + * Gets the list of storage buckets associated with a specific project from GCP. + * Ref: https://cloud.google.com/storage/docs/json_api/v1/buckets/list + * @param {string} bucketName name of the storage bucket + * @return a bucket resource object + */ +export async function listBuckets(projectId: string): Promise> { + try { + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + const result = await localAPIClient.get( + `/storage/v1/b?project=${projectId}`, + ); + return result.body.items.map((bucket: { name: string }) => bucket.name); + } catch (err: any) { + logger.debug(err); + throw new FirebaseError("Failed to read the storage buckets", { + original: err, + }); + } +} + /** * Find the service account for the Cloud Storage Resource * @param {string} projectId the project identifier - * * @returns: * { * "email_address": string, @@ -243,12 +294,12 @@ export async function getBucket(bucketName: string): Promise { */ export async function getServiceAccount(projectId: string): Promise { try { - const response = await api.request("GET", `/storage/v1/projects/${projectId}/serviceAccount`, { - auth: true, - origin: api.storageOrigin, - }); + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + const response = await localAPIClient.get( + `/storage/v1/projects/${projectId}/serviceAccount`, + ); return response.body; - } catch (err) { + } catch (err: any) { logger.debug(err); throw new FirebaseError("Failed to obtain the Cloud Storage service agent", { original: err, diff --git a/src/getDefaultHostingSite.ts b/src/getDefaultHostingSite.ts index aa931c0e029..6f94d580088 100644 --- a/src/getDefaultHostingSite.ts +++ b/src/getDefaultHostingSite.ts @@ -1,19 +1,36 @@ +import { FirebaseError } from "./error"; +import { SiteType, listSites } from "./hosting/api"; import { logger } from "./logger"; import { getFirebaseProject } from "./management/projects"; +import { needProjectId } from "./projectUtils"; +import { last } from "./utils"; + +export const errNoDefaultSite = new FirebaseError( + "Could not determine the default site for the project.", +); /** * Tries to determine the default hosting site for a project, else falls back to projectId. * @param options The command-line options object * @return The hosting site ID */ -export async function getDefaultHostingSite(options: any): Promise { - const project = await getFirebaseProject(options.project); - const site = project.resources?.hostingSite; +export async function getDefaultHostingSite(options: { projectId?: string }): Promise { + const projectId = needProjectId(options); + const project = await getFirebaseProject(projectId); + let site = project.resources?.hostingSite; if (!site) { - logger.debug( - `No default hosting site found for project: ${options.project}. Using projectId as hosting site name.` - ); - return options.project; + logger.debug(`the default site does not exist on the Firebase project; asking Hosting.`); + const sites = await listSites(projectId); + for (const s of sites) { + if (s.type === SiteType.DEFAULT_SITE) { + site = last(s.name.split("/")); + break; + } + } + if (!site) { + throw errNoDefaultSite; + } + return site; } return site; } diff --git a/src/handlePreviewToggles.ts b/src/handlePreviewToggles.ts index 04675ed8191..510270cf1e2 100644 --- a/src/handlePreviewToggles.ts +++ b/src/handlePreviewToggles.ts @@ -1,36 +1,51 @@ "use strict"; -import { unset, has } from "lodash"; -import { bold } from "cli-color"; +import { bold, red } from "colorette"; -import { configstore } from "./configstore"; -import { previews } from "./previews"; +import * as experiments from "./experiments"; -function _errorOut(name?: string) { - console.log(bold.red("Error:"), "Did not recognize preview feature", bold(name)); +function errorOut(name?: string): void { + console.log(`${bold(red("Error:"))} Did not recognize preview feature ${bold(name || "")}`); process.exit(1); } -export function handlePreviewToggles(args: string[]) { - const isValidPreview = has(previews, args[1]); +/** + * Implement --open-sesame and --close-sesame + */ +export function handlePreviewToggles(args: string[]): boolean { + const name = args[1]; + const isValid = experiments.isValidExperiment(name); if (args[0] === "--open-sesame") { - if (isValidPreview) { - console.log("Enabling preview feature", bold(args[1]) + "..."); - (previews as any)[args[1]] = true; - configstore.set("previews", previews); - console.log("Preview feature enabled!"); + console.log( + `${bold("firebase --open-sesame")} is deprecated and wil be removed in a future ` + + `version. Use the new "experiments" family of commands, including ${bold( + "firebase experiments:enable", + )}`, + ); + if (isValid) { + console.log(`Enabling experiment ${bold(name)} ...`); + experiments.setEnabled(name, true); + experiments.flushToDisk(); + console.log("Experiment enabled!"); return process.exit(0); } - _errorOut(); + errorOut(name); } else if (args[0] === "--close-sesame") { - if (isValidPreview) { - console.log("Disabling preview feature", bold(args[1])); - unset(previews, args[1]); - configstore.set("previews", previews); + console.log( + `${bold("firebase --open-sesame")} is deprecated and wil be removed in a future ` + + `version. Use the new "experiments" family of commands, including ${bold( + "firebase experiments:disable", + )}`, + ); + if (isValid) { + console.log(`Disabling experiment ${bold(name)}...`); + experiments.setEnabled(name, false); + experiments.flushToDisk(); return process.exit(0); } - _errorOut(); + errorOut(name); } + return false; } diff --git a/src/hosting/api.spec.ts b/src/hosting/api.spec.ts new file mode 100644 index 00000000000..009a1c2c78c --- /dev/null +++ b/src/hosting/api.spec.ts @@ -0,0 +1,936 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import { identityOrigin, hostingApiOrigin } from "../api"; +import { FirebaseError } from "../error"; +import * as hostingApi from "./api"; + +const TEST_CHANNELS_RESPONSE = { + channels: [ + // domain exists in TEST_GET_DOMAINS_RESPONSE + { url: "https://my-site--ch1-4iyrl1uo.web.app" }, + // domain does not exist in TEST_GET_DOMAINS_RESPONSE + // we assume this domain was manually removed by + // the user from the identity api + { url: "https://my-site--ch2-ygd8582v.web.app" }, + ], +}; +const TEST_GET_DOMAINS_RESPONSE = { + authorizedDomains: [ + "localhost", + "randomurl.com", + "my-site--ch1-4iyrl1uo.web.app", + // domain that should be removed + "my-site--expiredchannel-difhyc76.web.app", + ], +}; + +const EXPECTED_DOMAINS_RESPONSE = ["localhost", "randomurl.com", "my-site--ch1-4iyrl1uo.web.app"]; +const PROJECT_ID = "test-project"; +const SITE = "my-site"; + +const SITE_DOMAINS_API = `/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/domains`; + +// reuse domains from EXPECTED_DOMAINS_RESPONSE +const GET_SITE_DOMAINS_BODY = EXPECTED_DOMAINS_RESPONSE.map((domain) => ({ + site: `projects/${PROJECT_ID}/sites/${SITE}`, + domainName: domain, + updateTime: "2023-01-11T15:28:08.980038900Z", + provisioning: [ + { + certStatus: "CERT_ACTIVE", + dnsStatus: "DNS_MATCH", + expectedIps: ["0.0.0.0"], + }, + ], + status: "DOMAIN_ACTIVE", +})); + +describe("hosting", () => { + describe("getChannel", () => { + afterEach(nock.cleanAll); + + it("should make the API request for a channel", async () => { + const CHANNEL_ID = "my-channel"; + const CHANNEL = { name: "my-channel" }; + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`) + .reply(200, CHANNEL); + + const res = await hostingApi.getChannel(PROJECT_ID, SITE, CHANNEL_ID); + + expect(res).to.deep.equal({ name: "my-channel" }); + expect(nock.isDone()).to.be.true; + }); + + it("should return null if there's no channel", async () => { + const CHANNEL_ID = "my-channel"; + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`) + .reply(404, {}); + + const res = await hostingApi.getChannel(PROJECT_ID, SITE, CHANNEL_ID); + + expect(res).to.deep.equal(null); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const CHANNEL_ID = "my-channel"; + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.getChannel(PROJECT_ID, SITE, CHANNEL_ID), + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listChannels", () => { + afterEach(nock.cleanAll); + + it("should make a single API requests to list a small number of channels", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query({ pageToken: "", pageSize: 10 }) + .reply(200, { channels: [{ name: "channel01" }] }); + + const res = await hostingApi.listChannels(PROJECT_ID, SITE); + + expect(res).to.deep.equal([{ name: "channel01" }]); + expect(nock.isDone()).to.be.true; + }); + + it("should return 0 channels if none are returned", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query({ pageToken: "", pageSize: 10 }) + .reply(200, {}); + + const res = await hostingApi.listChannels(PROJECT_ID, SITE); + + expect(res).to.deep.equal([]); + expect(nock.isDone()).to.be.true; + }); + + it("should make multiple API requests to list channels", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query({ pageToken: "", pageSize: 10 }) + .reply(200, { channels: [{ name: "channel01" }], nextPageToken: "02" }); + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query({ pageToken: "02", pageSize: 10 }) + .reply(200, { channels: [{ name: "channel02" }] }); + + const res = await hostingApi.listChannels(PROJECT_ID, SITE); + + expect(res).to.deep.equal([{ name: "channel01" }, { name: "channel02" }]); + expect(nock.isDone()).to.be.true; + }); + + it("should return an error if there's no channel", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query({ pageToken: "", pageSize: 10 }) + .reply(404, {}); + + await expect(hostingApi.listChannels(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /could not find channels/, + ); + + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query({ pageToken: "", pageSize: 10 }) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.listChannels(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createChannel", () => { + afterEach(nock.cleanAll); + + it("should make the API request to create a channel", async () => { + const CHANNEL_ID = "my-channel"; + const CHANNEL = { name: "my-channel" }; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`, { ttl: "604800s" }) + .query({ channelId: CHANNEL_ID }) + .reply(201, CHANNEL); + + const res = await hostingApi.createChannel(PROJECT_ID, SITE, CHANNEL_ID); + + expect(res).to.deep.equal(CHANNEL); + expect(nock.isDone()).to.be.true; + }); + + it("should let us customize the TTL", async () => { + const CHANNEL_ID = "my-channel"; + const CHANNEL = { name: "my-channel" }; + const TTL = "60s"; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`, { ttl: TTL }) + .query({ channelId: CHANNEL_ID }) + .reply(201, CHANNEL); + + const res = await hostingApi.createChannel(PROJECT_ID, SITE, CHANNEL_ID, 60_000); + + expect(res).to.deep.equal(CHANNEL); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const CHANNEL_ID = "my-channel"; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`, { ttl: "604800s" }) + .query({ channelId: CHANNEL_ID }) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.createChannel(PROJECT_ID, SITE, CHANNEL_ID), + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateChannelTtl", () => { + afterEach(nock.cleanAll); + + it("should make the API request to update a channel", async () => { + const CHANNEL_ID = "my-channel"; + const CHANNEL = { name: "my-channel" }; + nock(hostingApiOrigin()) + .patch(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`, { + ttl: "604800s", + }) + .query({ updateMask: "ttl" }) + .reply(201, CHANNEL); + + const res = await hostingApi.updateChannelTtl(PROJECT_ID, SITE, CHANNEL_ID); + + expect(res).to.deep.equal(CHANNEL); + expect(nock.isDone()).to.be.true; + }); + + it("should let us customize the TTL", async () => { + const CHANNEL_ID = "my-channel"; + const CHANNEL = { name: "my-channel" }; + const TTL = "60s"; + nock(hostingApiOrigin()) + .patch(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`, { ttl: TTL }) + .query({ updateMask: "ttl" }) + .reply(201, CHANNEL); + + const res = await hostingApi.updateChannelTtl(PROJECT_ID, SITE, CHANNEL_ID, 60_000); + + expect(res).to.deep.equal(CHANNEL); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const CHANNEL_ID = "my-channel"; + nock(hostingApiOrigin()) + .patch(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`, { + ttl: "604800s", + }) + .query({ updateMask: "ttl" }) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.updateChannelTtl(PROJECT_ID, SITE, CHANNEL_ID), + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deleteChannel", () => { + afterEach(nock.cleanAll); + + it("should make the API request to delete a channel", async () => { + const CHANNEL_ID = "my-channel"; + const CHANNEL = { name: "my-channel" }; + nock(hostingApiOrigin()) + .delete(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`) + .reply(204, CHANNEL); + + const res = await hostingApi.deleteChannel(PROJECT_ID, SITE, CHANNEL_ID); + + expect(res).to.be.undefined; + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const CHANNEL_ID = "my-channel"; + nock(hostingApiOrigin()) + .delete(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${CHANNEL_ID}`) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.deleteChannel(PROJECT_ID, SITE, CHANNEL_ID), + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createVersion", () => { + afterEach(nock.cleanAll); + + it("should make the API requests to create a version", async () => { + const VERSION = { status: "CREATED" } as const; + const FULL_NAME = `projects/-/sites/${SITE}/versions/my-new-version`; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/-/sites/${SITE}/versions`, VERSION) + .reply(200, { name: FULL_NAME }); + + const res = await hostingApi.createVersion(SITE, VERSION); + + expect(res).to.deep.equal(FULL_NAME); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const VERSION = { status: "CREATED" } as const; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/-/sites/${SITE}/versions`, VERSION) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.createVersion(SITE, VERSION)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateVersion", () => { + afterEach(nock.cleanAll); + + it("should make the API requests to update a version", async () => { + const VERSION = { status: "FINALIZED" } as const; + nock(hostingApiOrigin()) + .patch(`/v1beta1/projects/-/sites/${SITE}/versions/my-version`, VERSION) + .query({ updateMask: "status" }) + .reply(200, VERSION); + + const res = await hostingApi.updateVersion(SITE, "my-version", VERSION); + + expect(res).to.deep.equal(VERSION); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const VERSION = { status: "FINALIZED" } as const; + nock(hostingApiOrigin()) + .patch(`/v1beta1/projects/-/sites/${SITE}/versions/my-version`, VERSION) + .query({ updateMask: "status" }) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.updateVersion(SITE, "my-version", VERSION), + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listVersions", () => { + afterEach(nock.cleanAll); + + const VERSION_1: hostingApi.Version = { + name: `projects/-/sites/${SITE}/versions/v1`, + status: "FINALIZED", + config: {}, + createTime: "now", + createUser: { + email: "inlined@google.com", + }, + fileCount: 0, + versionBytes: 0, + }; + const VERSION_2 = { + ...VERSION_1, + name: `projects/-/sites/${SITE}/versions/v2`, + }; + + it("returns no versions if no versions are returned", async () => { + nock(hostingApiOrigin()).get(`/v1beta1/projects/-/sites/${SITE}/versions`).reply(200, {}); + nock(hostingApiOrigin()); + + const versions = await hostingApi.listVersions(SITE); + expect(versions).deep.equals([]); + expect(nock.isDone()).to.be.true; + }); + + it("returns a single page of versions", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/-/sites/${SITE}/versions`) + .reply(200, { versions: [VERSION_1] }); + nock(hostingApiOrigin()); + + const versions = await hostingApi.listVersions(SITE); + expect(versions).deep.equals([VERSION_1]); + expect(nock.isDone()).to.be.true; + }); + + it("paginates through many versions", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/-/sites/${SITE}/versions`) + .reply(200, { versions: [VERSION_1], nextPageToken: "page2" }); + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/-/sites/${SITE}/versions?pageToken=page2`) + .reply(200, { versions: [VERSION_2] }); + + const versions = await hostingApi.listVersions(SITE); + expect(versions).deep.equals([VERSION_1, VERSION_2]); + expect(nock.isDone()).to.be.true; + }); + + it("handles errors", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/-/sites/${SITE}/versions`) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.listVersions(SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("cloneVersion", () => { + afterEach(nock.cleanAll); + + it("should make the API requests to clone a version", async () => { + const SOURCE_VERSION = "my-version"; + const VERSION = { name: "my-new-version" }; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/-/sites/${SITE}/versions:clone`, { + sourceVersion: SOURCE_VERSION, + finalize: false, + }) + .reply(200, { name: `projects/${PROJECT_ID}/operations/op` }); + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/operations/op`) + .reply(200, { + name: `projects/${PROJECT_ID}/operations/op`, + done: true, + response: VERSION, + }); + + const res = await hostingApi.cloneVersion(SITE, SOURCE_VERSION); + + expect(res).to.deep.equal(VERSION); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const SOURCE_VERSION = "my-version"; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/-/sites/${SITE}/versions:clone`, { + sourceVersion: SOURCE_VERSION, + finalize: false, + }) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.cloneVersion(SITE, SOURCE_VERSION)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createRelease", () => { + afterEach(nock.cleanAll); + + it("should make the API request to create a release", async () => { + const CHANNEL_ID = "my-channel"; + const RELEASE = { name: "my-new-release" }; + const VERSION = "version"; + const VERSION_NAME = `sites/${SITE}/versions/${VERSION}`; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/-/sites/${SITE}/channels/${CHANNEL_ID}/releases`) + .query({ versionName: VERSION_NAME }) + .reply(201, RELEASE); + + const res = await hostingApi.createRelease(SITE, CHANNEL_ID, VERSION_NAME); + + expect(res).to.deep.equal(RELEASE); + expect(nock.isDone()).to.be.true; + }); + + it("should include a message, if provided", async () => { + const CHANNEL_ID = "my-channel"; + const RELEASE = { name: "my-new-release" }; + const VERSION = "version"; + const VERSION_NAME = `sites/${SITE}/versions/${VERSION}`; + const MESSAGE = "yo dawg"; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/-/sites/${SITE}/channels/${CHANNEL_ID}/releases`, { + message: MESSAGE, + }) + .query({ versionName: VERSION_NAME }) + .reply(201, RELEASE); + + const res = await hostingApi.createRelease(SITE, CHANNEL_ID, VERSION_NAME, { + message: MESSAGE, + }); + + expect(res).to.deep.equal(RELEASE); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + const CHANNEL_ID = "my-channel"; + const VERSION = "VERSION"; + const VERSION_NAME = `sites/${SITE}/versions/${VERSION}`; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/-/sites/${SITE}/channels/${CHANNEL_ID}/releases`) + .query({ versionName: VERSION_NAME }) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.createRelease(SITE, CHANNEL_ID, VERSION_NAME), + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getSite", () => { + afterEach(nock.cleanAll); + + it("should make the API request for a channel", async () => { + const SITE_BODY = { name: "my-site" }; + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .reply(200, SITE_BODY); + + const res = await hostingApi.getSite(PROJECT_ID, SITE); + + expect(res).to.deep.equal(SITE_BODY); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the site doesn't exist", async () => { + nock(hostingApiOrigin()).get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`).reply(404, {}); + + await expect(hostingApi.getSite(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /could not find site/, + ); + + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.getSite(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listSites", () => { + afterEach(nock.cleanAll); + + it("should make a single API requests to list a small number of sites", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites`) + .query({ pageToken: "", pageSize: 10 }) + .reply(200, { sites: [{ name: "site01" }] }); + + const res = await hostingApi.listSites(PROJECT_ID); + + expect(res).to.deep.equal([{ name: "site01" }]); + expect(nock.isDone()).to.be.true; + }); + + it("should return no sites if none are returned", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites`) + .query({ pageToken: "", pageSize: 10 }) + .reply(200, {}); + + const res = await hostingApi.listSites(PROJECT_ID); + + expect(res).to.deep.equal([]); + expect(nock.isDone()).to.be.true; + }); + + it("should make multiple API requests to list sites", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites`) + .query({ pageToken: "", pageSize: 10 }) + .reply(200, { sites: [{ name: "site01" }], nextPageToken: "02" }); + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites`) + .query({ pageToken: "02", pageSize: 10 }) + .reply(200, { sites: [{ name: "site02" }] }); + + const res = await hostingApi.listSites(PROJECT_ID); + + expect(res).to.deep.equal([{ name: "site01" }, { name: "site02" }]); + expect(nock.isDone()).to.be.true; + }); + + it("should return an error if there's no site", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites`) + .query({ pageToken: "", pageSize: 10 }) + .reply(404, {}); + + await expect(hostingApi.listSites(PROJECT_ID)).to.eventually.be.rejectedWith( + FirebaseError, + /could not find sites/, + ); + + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites`) + .query({ pageToken: "", pageSize: 10 }) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.listSites(PROJECT_ID)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createSite", () => { + afterEach(nock.cleanAll); + + it("should make the API request to create a channel", async () => { + const SITE_BODY = { name: "my-new-site" }; + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/sites`, { appId: "" }) + .query({ siteId: SITE }) + .reply(201, SITE_BODY); + + const res = await hostingApi.createSite(PROJECT_ID, SITE); + + expect(res).to.deep.equal(SITE_BODY); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + nock(hostingApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/sites`, { appId: "" }) + .query({ siteId: SITE }) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.createSite(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateSite", () => { + const SITE_OBJ: hostingApi.Site = { + name: "my-site", + defaultUrl: "", + appId: "foo", + labels: {}, + }; + + afterEach(nock.cleanAll); + + it("should make the API request to update a site", async () => { + nock(hostingApiOrigin()) + .patch(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .query({ updateMask: "appId" }) + .reply(201, SITE_OBJ); + + const res = await hostingApi.updateSite(PROJECT_ID, SITE_OBJ, ["appId"]); + + expect(res).to.deep.equal(SITE_OBJ); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + nock(hostingApiOrigin()) + .patch(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .query({ updateMask: "appId" }) + .reply(500, { error: "server boo-boo" }); + + await expect( + hostingApi.updateSite(PROJECT_ID, SITE_OBJ, ["appId"]), + ).to.eventually.be.rejectedWith(FirebaseError, /server boo-boo/); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deleteSite", () => { + afterEach(nock.cleanAll); + + it("should make the API request to delete a site", async () => { + nock(hostingApiOrigin()) + .delete(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .reply(201, {}); + + const res = await hostingApi.deleteSite(PROJECT_ID, SITE); + + expect(res).to.be.undefined; + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + nock(hostingApiOrigin()) + .delete(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.deleteSite(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getCleanDomains", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("should return the list of expected auth domains after syncing", async () => { + // mock listChannels response + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query(() => true) + .reply(200, TEST_CHANNELS_RESPONSE); + // mock getAuthDomains response + nock(identityOrigin()) + .get(`/admin/v2/projects/${PROJECT_ID}/config`) + .reply(200, TEST_GET_DOMAINS_RESPONSE); + + const res = await hostingApi.getCleanDomains(PROJECT_ID, SITE); + + expect(res).to.deep.equal(EXPECTED_DOMAINS_RESPONSE); + expect(nock.isDone()).to.be.true; + }); + + it("should not remove sites that are similarly named", async () => { + // mock listChannels response + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) + .query(() => true) + .reply(200, { + channels: [ + { url: "https://my-site--ch1-4iyrl1uo.web.app" }, + { url: "https://my-site--ch2-ygd8582v.web.app" }, + ], + }); + // mock getAuthDomains response + nock(identityOrigin()) + .get(`/admin/v2/projects/${PROJECT_ID}/config`) + .reply(200, { + authorizedDomains: [ + "localhost", + "randomurl.com", + "my-site--ch1-4iyrl1uo.web.app", + "my-site--expiredchannel-difhyc76.web.app", + "backendof-my-site--some-abcd1234.web.app", + ], + }); + + const res = await hostingApi.getCleanDomains(PROJECT_ID, SITE); + + expect(res).to.deep.equal([ + "localhost", + "randomurl.com", + "my-site--ch1-4iyrl1uo.web.app", + "backendof-my-site--some-abcd1234.web.app", + ]); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getSiteDomains", () => { + afterEach(nock.cleanAll); + + it("should get the site domains", async () => { + nock(hostingApiOrigin()).get(SITE_DOMAINS_API).reply(200, { domains: GET_SITE_DOMAINS_BODY }); + + const res = await hostingApi.getSiteDomains(PROJECT_ID, SITE); + + expect(res).to.deep.equal(GET_SITE_DOMAINS_BODY); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the site doesn't exist", async () => { + nock(hostingApiOrigin()).get(SITE_DOMAINS_API).reply(404, {}); + + await expect(hostingApi.getSiteDomains(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /could not find site/, + ); + + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the server returns an error", async () => { + nock(hostingApiOrigin()).get(SITE_DOMAINS_API).reply(500, { error: "server boo-boo" }); + + await expect(hostingApi.getSiteDomains(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /server boo-boo/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getAllSiteDomains", () => { + afterEach(nock.cleanAll); + + it("should get the site domains", async () => { + nock(hostingApiOrigin()).get(SITE_DOMAINS_API).reply(200, { domains: GET_SITE_DOMAINS_BODY }); + + const GET_SITE_BODY = { + name: `projects/${PROJECT_ID}/sites/${SITE}`, + defaultUrl: EXPECTED_DOMAINS_RESPONSE[0], + type: "DEFAULT_SITE", + }; + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .reply(200, GET_SITE_BODY); + + const allDomainsPlusWebAppAndFirebaseApp = [ + ...EXPECTED_DOMAINS_RESPONSE, + `${SITE}.web.app`, + `${SITE}.firebaseapp.com`, + ]; + + expect(await hostingApi.getAllSiteDomains(PROJECT_ID, SITE)).to.have.members( + allDomainsPlusWebAppAndFirebaseApp, + ); + }); + + it("should throw an error if the site doesn't exist", async () => { + nock(hostingApiOrigin()).get(SITE_DOMAINS_API).reply(404, {}); + nock(hostingApiOrigin()).get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`).reply(404, {}); + + await expect(hostingApi.getAllSiteDomains(PROJECT_ID, SITE)).to.eventually.be.rejectedWith( + FirebaseError, + /could not find site/, + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getDeploymentDomain", () => { + afterEach(nock.cleanAll); + + it("should get the default site domain when hostingChannel is omitted", async () => { + const defaultDomain = EXPECTED_DOMAINS_RESPONSE[EXPECTED_DOMAINS_RESPONSE.length - 1]; + const defaultUrl = `https://${defaultDomain}`; + + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .reply(200, { defaultUrl }); + + expect(await hostingApi.getDeploymentDomain(PROJECT_ID, SITE)).to.equal(defaultDomain); + }); + + it("should get the default site domain when hostingChannel is undefined", async () => { + const defaultDomain = EXPECTED_DOMAINS_RESPONSE[EXPECTED_DOMAINS_RESPONSE.length - 1]; + const defaultUrl = `https://${defaultDomain}`; + + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`) + .reply(200, { defaultUrl }); + + expect(await hostingApi.getDeploymentDomain(PROJECT_ID, SITE, undefined)).to.equal( + defaultDomain, + ); + }); + + it("should get the channel domain", async () => { + const channelId = "my-channel"; + const channelDomain = `${PROJECT_ID}--${channelId}-123123.web.app`; + const channel = { url: `https://${channelDomain}` }; + + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${channelId}`) + .reply(200, channel); + + expect(await hostingApi.getDeploymentDomain(PROJECT_ID, SITE, channelId)).to.equal( + channelDomain, + ); + }); + + it("should return null if channel not found", async () => { + const channelId = "my-channel"; + + nock(hostingApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels/${channelId}`) + .reply(404, {}); + + expect(await hostingApi.getDeploymentDomain(PROJECT_ID, SITE, channelId)).to.be.null; + }); + + it("should return null if site not found", async () => { + nock(hostingApiOrigin()).get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}`).reply(404, {}); + + expect(await hostingApi.getDeploymentDomain(PROJECT_ID, SITE)).to.be.null; + }); + }); +}); + +describe("normalizeName", () => { + const tests = [ + { in: "happy-path", out: "happy-path" }, + { in: "feature/branch", out: "feature-branch" }, + { in: "featuRe/Branch", out: "featuRe-Branch" }, + { in: "what/are:you_thinking", out: "what-are-you-thinking" }, + { in: "happyBranch", out: "happyBranch" }, + { in: "happy:branch", out: "happy-branch" }, + { in: "happy_branch", out: "happy-branch" }, + { in: "happy#branch", out: "happy-branch" }, + ]; + + for (const t of tests) { + it(`should handle the normalization of ${t.in}`, () => { + expect(hostingApi.normalizeName(t.in)).to.equal(t.out); + }); + } +}); diff --git a/src/hosting/api.ts b/src/hosting/api.ts index 5110dc9c8df..4ef1dba73fd 100644 --- a/src/hosting/api.ts +++ b/src/hosting/api.ts @@ -4,6 +4,8 @@ import { Client } from "../apiv2"; import * as operationPoller from "../operation-poller"; import { DEFAULT_DURATION } from "../hosting/expireUtils"; import { getAuthDomains, updateAuthDomains } from "../gcp/auth"; +import * as proto from "../gcp/proto"; +import { getHostnameFromUrl } from "../utils"; const ONE_WEEK_MS = 604800000; // 7 * 24 * 60 * 60 * 1000 @@ -13,7 +15,7 @@ interface ActingUser { // A profile image URL for the user. May not be present if the user has // changed their email address or deleted their account. - imageUrl: string; + imageUrl?: string; } enum ReleaseType { @@ -30,14 +32,14 @@ enum ReleaseType { SITE_DISABLE = "SITE_DISABLE", } -interface Release { +export interface Release { // The unique identifier for the release, in the format: // sites/site-name/releases/releaseID readonly name: string; // The configuration and content that was released. // TODO: create a Version type interface. - readonly version: any; // eslint-disable-line @typescript-eslint/no-explicit-any + readonly version: Version; // Explains the reason for the release. // Specify a value for this field only when creating a `SITE_DISABLE` @@ -87,30 +89,76 @@ export interface Channel { labels: { [key: string]: string }; } -enum VersionStatus { - // The default status; should not be intentionally used. - VERSION_STATUS_UNSPECIFIED = "VERSION_STATUS_UNSPECIFIED", +export interface Domain { + site: `projects/${string}/sites/${string}`; + domainName: string; + updateTime: string; + provisioning?: { + certStatus: string; + dnsStatus: string; + expectedIps: string[]; + }; + status: string; + domainRedirect?: { + domainName: string; + type: string; + }; +} + +export type VersionStatus = // The version has been created, and content is currently being added to the // version. - CREATED = "CREATED", + | "CREATED" // All content has been added to the version, and the version can no longer be // changed. - FINALIZED = "FINALIZED", + | "FINALIZED" // The version has been deleted. - DELETED = "DELETED", + | "DELETED" // The version was not updated to `FINALIZED` within 12 hours and was // automatically deleted. - ABANDONED = "ABANDONED", + | "ABANDONED" // The version is outside the site-configured limit for the number of // retained versions, so the version's content is scheduled for deletion. - EXPIRED = "EXPIRED", + | "EXPIRED" // The version is being cloned from another version. All content is still // being copied over. - CLONING = "CLONING", + | "CLONING"; + +export type HasPattern = { glob: string } | { regex: string }; + +export type Header = HasPattern & { + regex?: string; + headers: Record; +}; + +export type Redirect = HasPattern & { + statusCode?: number; + location: string; +}; + +export interface RunRewrite { + serviceId: string; + region: string; + tag?: string; } -// TODO: define ServingConfig. -enum ServingConfig {} +export type RewriteBehavior = + | { path: string } + | { function: string; functionRegion?: string } + | { dynamicLinks: true } + | { run: RunRewrite }; + +export type Rewrite = HasPattern & RewriteBehavior; + +export interface ServingConfig { + headers?: Header[]; + redirects?: Redirect[]; + rewrites?: Rewrite[]; + cleanUrls?: boolean; + trailingSlashBehavior?: "ADD" | "REMOVE"; + appAssociation?: "AUTO" | "NONE"; + i18n?: { root: string }; +} export interface Version { // The unique identifier for a version, in the format: @@ -121,10 +169,10 @@ export interface Version { status: VersionStatus; // The configuration for the behavior of the site. - config: ServingConfig; + config?: ServingConfig; // The labels used for extra metadata and/or filtering. - labels: Map; + labels?: Record; // The time at which the version was created. readonly createTime: string; @@ -133,16 +181,16 @@ export interface Version { readonly createUser: ActingUser; // The time at which the version was `FINALIZED`. - readonly finalizeTime: string; + readonly finalizeTime?: string; // Identifies the user who `FINALIZED` the version. - readonly finalizeUser: ActingUser; + readonly finalizeUser?: ActingUser; // The time at which the version was `DELETED`. - readonly deleteTime: string; + readonly deleteTime?: string; // Identifies the user who `DELETED` the version. - readonly deleteUser: ActingUser; + readonly deleteUser?: ActingUser; // The total number of files associated with the version. readonly fileCount: number; @@ -151,6 +199,17 @@ export interface Version { readonly versionBytes: number; } +export type VERSION_OUTPUT_FIELDS = + | "name" + | "createTime" + | "createUser" + | "finalizeTime" + | "finalizeUser" + | "deleteTime" + | "deleteUser" + | "fileCount" + | "versionBytes"; + interface CloneVersionRequest { // The name of the version to be cloned, in the format: // `sites/{site}/versions/{version}` @@ -160,15 +219,17 @@ interface CloneVersionRequest { finalize?: boolean; } -interface LongRunningOperation { - // The identifier of the Operation. - readonly name: string; +// The possible types of a site. +export enum SiteType { + // Unknown state, likely the result of an error on the backend. + TYPE_UNSPECIFIED = "TYPE_UNSPECIFIED", - // Set to `true` if the Operation is done. - readonly done: boolean; + // The default Hosting site that is provisioned when a Firebase project is + // created. + DEFAULT_SITE = "DEFAULT_SITE", - // Additional metadata about the Operation. - readonly metadata: T | undefined; + // A Hosting site that the user created. + USER_SITE = "USER_SITE", } export type Site = { @@ -179,6 +240,8 @@ export type Site = { readonly appId: string; + readonly type?: SiteType; + labels: { [key: string]: string }; }; @@ -195,7 +258,7 @@ export function normalizeName(s: string): string { } const apiClient = new Client({ - urlPrefix: hostingApiOrigin, + urlPrefix: hostingApiOrigin(), apiVersion: "v1beta1", auth: true, }); @@ -210,15 +273,15 @@ const apiClient = new Client({ export async function getChannel( project: string | number = "-", site: string, - channelId: string + channelId: string, ): Promise { try { const res = await apiClient.get( - `/projects/${project}/sites/${site}/channels/${channelId}` + `/projects/${project}/sites/${site}/channels/${channelId}`, ); return res.body; - } catch (e) { - if (e.status === 404) { + } catch (e: unknown) { + if (e instanceof FirebaseError && e.status === 404) { return null; } throw e; @@ -232,26 +295,23 @@ export async function getChannel( */ export async function listChannels( project: string | number = "-", - site: string + site: string, ): Promise { const channels: Channel[] = []; let nextPageToken = ""; for (;;) { try { - const res = await apiClient.get<{ nextPageToken?: string; channels: Channel[] }>( + const res = await apiClient.get<{ nextPageToken?: string; channels?: Channel[] }>( `/projects/${project}/sites/${site}/channels`, - { queryParams: { pageToken: nextPageToken, pageSize: 10 } } + { queryParams: { pageToken: nextPageToken, pageSize: 10 } }, ); - const c = res.body?.channels; - if (c) { - channels.push(...c); - } - nextPageToken = res.body?.nextPageToken || ""; + channels.push(...(res.body.channels ?? [])); + nextPageToken = res.body.nextPageToken || ""; if (!nextPageToken) { return channels; } - } catch (e) { - if (e.status === 404) { + } catch (e: unknown) { + if (e instanceof FirebaseError && e.status === 404) { throw new FirebaseError(`could not find channels for site "${site}"`, { original: e, }); @@ -272,11 +332,11 @@ export async function createChannel( project: string | number = "-", site: string, channelId: string, - ttlMillis: number = DEFAULT_DURATION + ttlMillis: number = DEFAULT_DURATION, ): Promise { const res = await apiClient.post<{ ttl: string }, Channel>( `/projects/${project}/sites/${site}/channels?channelId=${channelId}`, - { ttl: `${ttlMillis / 1000}s` } + { ttl: `${ttlMillis / 1000}s` }, ); return res.body; } @@ -292,12 +352,12 @@ export async function updateChannelTtl( project: string | number = "-", site: string, channelId: string, - ttlMillis: number = ONE_WEEK_MS + ttlMillis: number = ONE_WEEK_MS, ): Promise { const res = await apiClient.patch<{ ttl: string }, Channel>( `/projects/${project}/sites/${site}/channels/${channelId}`, { ttl: `${ttlMillis / 1000}s` }, - { queryParams: { updateMask: ["ttl"].join(",") } } + { queryParams: { updateMask: "ttl" } }, ); return res.body; } @@ -311,11 +371,76 @@ export async function updateChannelTtl( export async function deleteChannel( project: string | number = "-", site: string, - channelId: string + channelId: string, ): Promise { await apiClient.delete(`/projects/${project}/sites/${site}/channels/${channelId}`); } +/** + * Creates a version + */ +export async function createVersion( + siteId: string, + version: Omit, +): Promise { + const res = await apiClient.post( + `projects/-/sites/${siteId}/versions`, + version, + ); + return res.body.name; +} + +/** + * Updates a version. + */ +export async function updateVersion( + site: string, + versionId: string, + version: Partial, +): Promise { + const res = await apiClient.patch, Version>( + `projects/-/sites/${site}/versions/${versionId}`, + version, + { + queryParams: { + // N.B. It's not clear why we need "config". If the Hosting server acted + // like a normal OP service, we could update config.foo and config.bar + // in a PATCH command even if config was the empty object already. But + // not setting config in createVersion and then setting config subfields + // in updateVersion is failing with + // "HTTP Error: 40 Unknown path in `updateMask`: `config.rewrites`" + updateMask: proto.fieldMasks(version, "labels", "config").join(","), + }, + }, + ); + return res.body; +} + +interface ListVersionsResponse { + versions?: Version[]; + nextPageToken?: string; +} + +/** + * Get a list of all versions for a site, automatically handling pagination. + */ +export async function listVersions(site: string): Promise { + let pageToken: string | undefined = undefined; + const versions: Version[] = []; + do { + const queryParams: Record = {}; + if (pageToken) { + queryParams.pageToken = pageToken; + } + const res = await apiClient.get(`projects/-/sites/${site}/versions`, { + queryParams, + }); + versions.push(...(res.body.versions ?? [])); + pageToken = res.body.nextPageToken; + } while (pageToken); + return versions; +} + /** * Create a version a clone. * @param site the site for the version. @@ -325,18 +450,18 @@ export async function deleteChannel( export async function cloneVersion( site: string, versionName: string, - finalize = false + finalize = false, ): Promise { - const res = await apiClient.post>( - `/projects/-/sites/${site}/versions:clone`, - { - sourceVersion: versionName, - finalize, - } - ); + const res = await apiClient.post< + CloneVersionRequest, + operationPoller.LongRunningOperation + >(`/projects/-/sites/${site}/versions:clone`, { + sourceVersion: versionName, + finalize, + }); const { name: operationName } = res.body; const pollRes = await operationPoller.pollOperation({ - apiOrigin: hostingApiOrigin, + apiOrigin: hostingApiOrigin(), apiVersion: "v1beta1", operationResourceName: operationName, masterTimeout: 600000, @@ -344,6 +469,8 @@ export async function cloneVersion( return pollRes; } +type PartialRelease = Partial>; + /** * Create a release on a channel. * @param site the site for the version. @@ -353,13 +480,14 @@ export async function cloneVersion( export async function createRelease( site: string, channel: string, - version: string + version: string, + partialRelease?: PartialRelease, ): Promise { - const res = await apiClient.request({ - method: "POST", - path: `/projects/-/sites/${site}/channels/${channel}/releases`, - queryParams: { versionName: version }, - }); + const res = await apiClient.post( + `/projects/-/sites/${site}/channels/${channel}/releases`, + partialRelease, + { queryParams: { versionName: version } }, + ); return res.body; } @@ -373,20 +501,17 @@ export async function listSites(project: string): Promise { let nextPageToken = ""; for (;;) { try { - const res = await apiClient.get<{ sites: Site[]; nextPageToken?: string }>( + const res = await apiClient.get<{ sites?: Site[]; nextPageToken?: string }>( `/projects/${project}/sites`, - { queryParams: { pageToken: nextPageToken, pageSize: 10 } } + { queryParams: { pageToken: nextPageToken, pageSize: 10 } }, ); - const c = res.body?.sites; - if (c) { - sites.push(...c); - } - nextPageToken = res.body?.nextPageToken || ""; + sites.push(...(res.body.sites ?? [])); + nextPageToken = res.body.nextPageToken || ""; if (!nextPageToken) { return sites; } - } catch (e) { - if (e.status === 404) { + } catch (e: unknown) { + if (e instanceof FirebaseError && e.status === 404) { throw new FirebaseError(`could not find sites for project "${project}"`, { original: e, }); @@ -396,6 +521,20 @@ export async function listSites(project: string): Promise { } } +/** + * Get fake sites object for demo projects running with emulator + */ +export function listDemoSites(projectId: string): Site[] { + return [ + { + name: `projects/${projectId}/sites/${projectId}`, + defaultUrl: `https://${projectId}.firebaseapp.com`, + appId: "fake-app-id", + labels: {}, + }, + ]; +} + /** * Get a Hosting site. * @param project project name or number. @@ -406,10 +545,11 @@ export async function getSite(project: string, site: string): Promise { try { const res = await apiClient.get(`/projects/${project}/sites/${site}`); return res.body; - } catch (e) { - if (e.status === 404) { + } catch (e: unknown) { + if (e instanceof FirebaseError && e.status === 404) { throw new FirebaseError(`could not find site "${site}" for project "${project}"`, { original: e, + status: e.status, }); } throw e; @@ -423,11 +563,20 @@ export async function getSite(project: string, site: string): Promise { * @param appId the Firebase Web App ID (https://firebase.google.com/docs/projects/learn-more#config-files-objects) * @return site information. */ -export async function createSite(project: string, site: string, appId = ""): Promise { +export async function createSite( + project: string, + site: string, + appId = "", + validateOnly = false, +): Promise { + const queryParams: Record = { siteId: site }; + if (validateOnly) { + queryParams.validateOnly = "true"; + } const res = await apiClient.post<{ appId: string }, Site>( `/projects/${project}/sites`, { appId: appId }, - { queryParams: { site_id: site } } + { queryParams }, ); return res.body; } @@ -485,7 +634,7 @@ export async function removeAuthDomain(project: string, url: string): Promise domain != targetDomain); + const authDomains = domains.filter((domain: string) => domain !== targetDomain); return updateAuthDomains(project, authDomains); } @@ -507,8 +656,8 @@ export async function getCleanDomains(project: string, site: string): Promise>> { const siteDomainMap = new Map(); for (const site of sites) { @@ -552,3 +701,84 @@ export async function cleanAuthState( } return siteDomainMap; } + +/** + * Retrieves all site domains + * + * @param project project ID + * @param site site id + * @return array of domains + */ +export async function getSiteDomains(project: string, site: string): Promise { + try { + const res = await apiClient.get<{ domains: Domain[] }>( + `/projects/${project}/sites/${site}/domains`, + ); + + return res.body.domains ?? []; + } catch (e: unknown) { + if (e instanceof FirebaseError && e.status === 404) { + throw new FirebaseError(`could not find site "${site}" for project "${project}"`, { + original: e, + }); + } + throw e; + } +} + +/** + * Join the default domain and the custom domains of a Hosting site + * + * @param projectId the project id + * @param siteId the site id + * @return array of domains + */ +export async function getAllSiteDomains(projectId: string, siteId: string): Promise { + const [hostingDomains, defaultDomain] = await Promise.all([ + getSiteDomains(projectId, siteId), + getSite(projectId, siteId), + ]); + + const defaultDomainWithoutHttp = defaultDomain.defaultUrl.replace(/^https?:\/\//, ""); + + const allSiteDomains = new Set([ + ...hostingDomains.map(({ domainName }) => domainName), + defaultDomainWithoutHttp, + `${siteId}.web.app`, + `${siteId}.firebaseapp.com`, + ]); + + return Array.from(allSiteDomains); +} + +/** + * Get the deployment domain. + * If hostingChannel is provided, get the channel url, otherwise get the + * default site url. + */ +export async function getDeploymentDomain( + projectId: string, + siteId: string, + hostingChannel?: string | undefined, +): Promise { + if (hostingChannel) { + const channel = await getChannel(projectId, siteId, hostingChannel); + + return channel && getHostnameFromUrl(channel?.url); + } + + const site = await getSite(projectId, siteId).catch((e: unknown) => { + // return null if the site doesn't exist + if ( + e instanceof FirebaseError && + e.original instanceof FirebaseError && + e.original.status === 404 + ) { + return null; + } + + throw e; + }); + + return site && getHostnameFromUrl(site?.defaultUrl); +} diff --git a/src/test/hosting/cloudRunProxy.spec.ts b/src/hosting/cloudRunProxy.spec.ts similarity index 94% rename from src/test/hosting/cloudRunProxy.spec.ts rename to src/hosting/cloudRunProxy.spec.ts index 88d0b30a31f..75e21819372 100644 --- a/src/test/hosting/cloudRunProxy.spec.ts +++ b/src/hosting/cloudRunProxy.spec.ts @@ -4,11 +4,8 @@ import * as nock from "nock"; import * as sinon from "sinon"; import * as supertest from "supertest"; -import { cloudRunApiOrigin } from "../../api"; -import cloudRunProxy, { - CloudRunProxyOptions, - CloudRunProxyRewrite, -} from "../../hosting/cloudRunProxy"; +import { cloudRunApiOrigin } from "../api"; +import cloudRunProxy, { CloudRunProxyOptions, CloudRunProxyRewrite } from "./cloudRunProxy"; describe("cloudRunProxy", () => { const fakeOptions: CloudRunProxyOptions = { @@ -35,7 +32,7 @@ describe("cloudRunProxy", () => { }); it("should error when the Cloud Run service doesn't exist", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/empty") .reply(404, { error: "service doesn't exist" }); @@ -52,7 +49,7 @@ describe("cloudRunProxy", () => { }); it("should error when the Cloud Run service doesn't exist", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/badService") .reply(200, { status: {} }); @@ -69,7 +66,7 @@ describe("cloudRunProxy", () => { }).timeout(2500); it("should resolve a function returns middleware that proxies to the live version", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin).get("/").reply(200, "live version"); @@ -87,7 +84,7 @@ describe("cloudRunProxy", () => { }); it("should pass on provided headers to the origin", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin, { reqheaders: { "x-custom-header": "cooooookie-crisp" } }) @@ -108,7 +105,7 @@ describe("cloudRunProxy", () => { }); it("should not send the `host` header if it's provided", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin, { @@ -135,7 +132,7 @@ describe("cloudRunProxy", () => { it("should resolve to a live version in another region", async () => { const cloudRunServiceOriginAsia = "https://helloworld-hash-as.a.run.app"; - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/asia-southeast1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOriginAsia } }); nock(cloudRunServiceOriginAsia).get("/").reply(200, "live version"); @@ -154,7 +151,7 @@ describe("cloudRunProxy", () => { it("should cache calls to look up Cloud Run service URLs", async () => { const multiCallOrigin = "https://multiLookup-hash-uc.a.run.app"; - const multiNock = nock(cloudRunApiOrigin) + const multiNock = nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/multiLookup") .reply(200, { status: { url: multiCallOrigin } }); nock(multiCallOrigin) @@ -171,7 +168,7 @@ describe("cloudRunProxy", () => { expect(multiNock.isDone()).to.be.true; // New rewrite for the same Cloud Run service - const failMultiNock = nock(cloudRunApiOrigin) + const failMultiNock = nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/multiLookup") .reply(500, "should not happen"); @@ -190,7 +187,7 @@ describe("cloudRunProxy", () => { }); it("should pass through normal 404 errors", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin).get("/404.html").reply(404, "normal 404"); @@ -208,7 +205,7 @@ describe("cloudRunProxy", () => { }); it("should do nothing on 404 errors with x-cascade", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin) @@ -235,7 +232,7 @@ describe("cloudRunProxy", () => { }); it("should remove cookies on non-private cached responses", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin) @@ -256,7 +253,7 @@ describe("cloudRunProxy", () => { }); it("should add required Vary headers to the response", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin) @@ -277,7 +274,7 @@ describe("cloudRunProxy", () => { }); it("should respond with a 500 error if an error occurs calling the Cloud Run service", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin).get("/500").replyWithError({ message: "normal error" }); @@ -295,7 +292,7 @@ describe("cloudRunProxy", () => { }); it("should respond with a 504 error if a timeout error occurs calling the Cloud Run service", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin) @@ -315,7 +312,7 @@ describe("cloudRunProxy", () => { }); it("should respond with a 504 error if a sockettimeout error occurs calling the Cloud Run service", async () => { - nock(cloudRunApiOrigin) + nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/helloworld") .reply(200, { status: { url: cloudRunServiceOrigin } }); nock(cloudRunServiceOrigin) diff --git a/src/hosting/cloudRunProxy.ts b/src/hosting/cloudRunProxy.ts index 24d536a146a..c0bbb7ccb2b 100644 --- a/src/hosting/cloudRunProxy.ts +++ b/src/hosting/cloudRunProxy.ts @@ -1,11 +1,11 @@ import { RequestHandler } from "express"; -import { get } from "lodash"; +import { Client } from "../apiv2"; +import { cloudRunApiOrigin } from "../api"; import { errorRequestHandler, proxyRequestHandler } from "./proxy"; -import { needProjectId } from "../projectUtils"; +import { FirebaseError } from "../error"; import { logger } from "../logger"; -import { cloudRunApiOrigin, request as apiRequest } from "../api"; -import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; export interface CloudRunProxyOptions { project?: string; @@ -20,30 +20,32 @@ export interface CloudRunProxyRewrite { const cloudRunCache: { [s: string]: string } = {}; -function getCloudRunUrl(rewrite: CloudRunProxyRewrite, projectId: string): Promise { +const apiClient = new Client({ urlPrefix: cloudRunApiOrigin(), apiVersion: "v1" }); + +async function getCloudRunUrl(rewrite: CloudRunProxyRewrite, projectId: string): Promise { const alreadyFetched = cloudRunCache[`${rewrite.run.region}/${rewrite.run.serviceId}`]; if (alreadyFetched) { return Promise.resolve(alreadyFetched); } - const path = `/v1/projects/${projectId}/locations/${ - rewrite.run.region || "us-central1" - }/services/${rewrite.run.serviceId}`; - logger.info(`[hosting] Looking up Cloud Run service "${path}" for its URL`); - return apiRequest("GET", path, { origin: cloudRunApiOrigin, auth: true }) - .then((res) => { - const url = get(res, "body.status.url"); - if (!url) { - return Promise.reject("Cloud Run URL doesn't exist in response."); - } + const path = `/projects/${projectId}/locations/${rewrite.run.region || "us-central1"}/services/${ + rewrite.run.serviceId + }`; + try { + logger.info(`[hosting] Looking up Cloud Run service "${path}" for its URL`); + const res = await apiClient.get<{ status?: { url?: string } }>(path); + const url = res.body.status?.url; + if (!url) { + throw new FirebaseError("Cloud Run URL doesn't exist in response."); + } - cloudRunCache[`${rewrite.run.region}/${rewrite.run.serviceId}`] = url; - return url; - }) - .catch((err) => { - const errInfo = `error looking up URL for Cloud Run service: ${err}`; - return Promise.reject(errInfo); + cloudRunCache[`${rewrite.run.region}/${rewrite.run.serviceId}`] = url; + return url; + } catch (err: any) { + throw new FirebaseError(`Error looking up URL for Cloud Run service: ${err}`, { + original: err, }); + } } /** @@ -52,7 +54,7 @@ function getCloudRunUrl(rewrite: CloudRunProxyRewrite, projectId: string): Promi * the live Cloud Run service running within the given project. */ export default function ( - options: CloudRunProxyOptions + options: CloudRunProxyOptions, ): (r: CloudRunProxyRewrite) => Promise { return async (rewrite: CloudRunProxyRewrite) => { if (!rewrite.run) { diff --git a/src/hosting/config.spec.ts b/src/hosting/config.spec.ts new file mode 100644 index 00000000000..c64a3f20fa0 --- /dev/null +++ b/src/hosting/config.spec.ts @@ -0,0 +1,415 @@ +import { expect } from "chai"; +import { FirebaseError } from "../error"; +import { HostingConfig, HostingMultiple, HostingSingle } from "../firebaseConfig"; + +import * as config from "./config"; +import { HostingOptions } from "./options"; +import { cloneDeep } from "../utils"; +import { setEnabled } from "../experiments"; +import { FIREBASE_JSON_PATH, FIXTURE_DIR } from "../test/fixtures/simplehosting"; + +function options( + hostingConfig: HostingConfig, + base?: Omit, + targetsToSites?: Record, +): HostingOptions { + return { + project: "project", + config: { + src: { + hosting: hostingConfig, + }, + }, + rc: { + requireTarget: (project: string, type: string, name: string): string[] => { + return targetsToSites?.[name] || []; + }, + }, + cwd: FIXTURE_DIR, + configPath: FIREBASE_JSON_PATH, + ...base, + }; +} + +describe("config", () => { + describe("extract", () => { + it("should handle no hosting config", () => { + const opts = options({}); + delete opts.config.src.hosting; + expect(config.extract(opts)).to.deep.equal([]); + }); + + it("should fail if both site and target are specified", () => { + const singleSiteOpts = options({ site: "site", target: "target" }); + expect(() => config.extract(singleSiteOpts)).throws( + FirebaseError, + /configs should only include either/, + ); + + const manySiteOpts = options([{ site: "site", target: "target" }]); + expect(() => config.extract(manySiteOpts)).throws( + FirebaseError, + /configs should only include either/, + ); + }); + + it("should always return an array", () => { + const single: HostingMultiple[number] = { site: "site" }; + let extracted = config.extract(options(single)); + expect(extracted).to.deep.equal([single]); + + extracted = config.extract(options([single])); + expect(extracted).to.deep.equal([single]); + }); + + it("should support legacy method of specifying site", () => { + const opts = options({}, { site: "legacy-site" }); + const extracted = config.extract(opts); + expect(extracted).to.deep.equal([{ site: "legacy-site" }]); + }); + }); + + describe("resolveTargets", () => { + it("should not modify the config", () => { + const cfg: HostingMultiple = [{ target: "target" }]; + const opts = options(cfg, {}, { target: ["site"] }); + config.resolveTargets(cfg, opts); + expect(cfg).to.deep.equal([{ target: "target" }]); + }); + + it("should add sites when found", () => { + const cfg: HostingMultiple = [{ target: "target" }]; + const opts = options(cfg, {}, { target: ["site"] }); + const resolved = config.resolveTargets(cfg, opts); + expect(resolved).to.deep.equal([{ target: "target", site: "site" }]); + }); + + // Note: Not testing the case where the target cannot be found because this + // exception comes out of the RC class, which is being mocked in tests. + + it("should prohibit multiple sites", () => { + const cfg: HostingMultiple = [{ target: "target" }]; + const opts = options(cfg, {}, { target: ["site", "other-site"] }); + expect(() => config.resolveTargets(cfg, opts)).to.throw( + FirebaseError, + /is linked to multiple sites, but only one is permitted/, + ); + }); + }); + + describe("filterOnly", () => { + const tests: Array< + { + desc: string; + cfg: HostingMultiple; + only?: string; + } & ({ want: HostingMultiple } | { wantErr: RegExp }) + > = [ + { + desc: "a normal hosting config, specifying the default site", + cfg: [{ site: "site" }], + only: "hosting:site", + want: [{ site: "site" }], + }, + { + desc: "a hosting config with multiple sites, no targets, specifying the second site", + cfg: [{ site: "site" }, { site: "different-site" }], + only: `hosting:different-site`, + want: [{ site: "different-site" }], + }, + { + desc: "a normal hosting config with a target", + cfg: [{ target: "main" }, { site: "site" }], + only: "hosting:main", + want: [{ target: "main" }], + }, + { + desc: "a hosting config with multiple targets, specifying one", + cfg: [{ target: "t-one" }, { target: "t-two" }], + only: "hosting:t-two", + want: [{ target: "t-two" }], + }, + { + desc: "a hosting config with multiple targets, specifying all hosting", + cfg: [{ target: "t-one" }, { target: "t-two" }], + only: "hosting", + want: [{ target: "t-one" }, { target: "t-two" }], + }, + { + desc: "a hosting config with multiple targets, specifying an invalid target", + cfg: [{ target: "t-one" }, { target: "t-two" }], + only: "hosting:t-three", + wantErr: /Hosting site or target.+t-three.+not detected/, + }, + { + desc: "a hosting config with multiple sites but no targets, only an invalid target", + cfg: [{ site: "s-one" }], + only: "hosting:t-one", + wantErr: /Hosting site or target.+t-one.+not detected/, + }, + { + desc: "a hosting config without an only string", + cfg: [{ site: "site" }], + want: [{ site: "site" }], + }, + { + desc: "a hosting config with a non-hosting only flag", + cfg: [{ site: "site" }], + only: "functions", + want: [], + }, + ]; + + for (const t of tests) { + it(`should be able to parse ${t.desc}`, () => { + if ("wantErr" in t) { + expect(() => config.filterOnly(t.cfg, t.only)).to.throw(FirebaseError, t.wantErr); + } else { + const got = config.filterOnly(t.cfg, t.only); + expect(got).to.deep.equal(t.want); + } + }); + } + }); + + describe("with an except parameter, resolving targets", () => { + const tests: Array< + { + desc: string; + cfg: HostingMultiple; + except?: string; + } & ({ want: HostingMultiple } | { wantErr: RegExp }) + > = [ + { + desc: "a hosting config with multiple sites, no targets, omitting the second site", + cfg: [{ site: "default-site" }, { site: "different-site" }], + except: `hosting:different-site`, + want: [{ site: "default-site" }], + }, + { + desc: "a normal hosting config with a target, omitting the target", + cfg: [{ target: "main" }], + except: "hosting:main", + want: [], + }, + { + desc: "a hosting config with multiple targets, omitting one", + cfg: [{ target: "t-one" }, { target: "t-two" }], + except: "hosting:t-two", + want: [{ target: "t-one" }], + }, + { + desc: "a hosting config with multiple targets, omitting all hosting", + cfg: [{ target: "t-one" }, { target: "t-two" }], + except: "hosting", + want: [], + }, + { + desc: "a hosting config with multiple targets, omitting an invalid target", + cfg: [{ target: "t-one" }, { target: "t-two" }], + except: "hosting:t-three", + want: [{ target: "t-one" }, { target: "t-two" }], + }, + { + desc: "a hosting config with no excpet string", + cfg: [{ target: "target" }], + want: [{ target: "target" }], + }, + { + desc: "a hosting config with a non-hosting except string", + cfg: [{ target: "target" }], + except: "functions", + want: [{ target: "target" }], + }, + ]; + + for (const t of tests) { + it(`should be able to parse ${t.desc}`, () => { + if ("wantErr" in t) { + expect(() => config.filterExcept(t.cfg, t.except)).to.throw(FirebaseError, t.wantErr); + } else { + const got = config.filterExcept(t.cfg, t.except); + expect(got).to.deep.equal(t.want); + } + }); + } + }); + + it("normalize", () => { + it("upgrades function configs", () => { + const configs: HostingMultiple = [ + { + site: "site", + public: "public", + rewrites: [ + { + glob: "**", + function: "functionId", + }, + { + glob: "**", + function: "function2", + region: "region", + }, + ], + }, + ]; + config.normalize(configs); + expect(configs).to.deep.equal([ + { + site: "site", + public: "public", + rewrites: [ + { + glob: "**", + function: { + functionid: "functionId", + }, + }, + { + glob: "**", + function: { + functionId: "function2", + region: "region", + }, + }, + ], + }, + ]); + }); + + it("leaves other rewrites alone", () => { + const configs: HostingMultiple = [ + { + site: "site", + public: "public", + rewrites: [ + { + glob: "**", + destination: "index.html", + }, + { + glob: "**", + function: { + functionId: "functionId", + }, + }, + { + glob: "**", + run: { + serviceId: "service", + }, + }, + { + glob: "**", + dynamicLinks: true, + }, + ], + }, + ]; + const expected = cloneDeep(configs); + config.normalize(configs); + expect(configs).to.deep.equal(expected); + }); + }); + + const PUBLIC_DIR_ERROR_PREFIX = /Must supply a "public" or "source" directory/; + describe("validate", () => { + const tests: Array<{ + desc: string; + site: HostingSingle; + wantErr?: RegExp; + }> = [ + { + desc: "should error out if there is no public directory but a 'destination' rewrite", + site: { + rewrites: [ + { source: "/foo", destination: "/bar.html" }, + { source: "/baz", function: "app" }, + ], + }, + wantErr: PUBLIC_DIR_ERROR_PREFIX, + }, + { + desc: "should error out if there is no public directory and an i18n with root", + site: { + i18n: { root: "/foo" }, + rewrites: [{ source: "/foo", function: "pass" }], + }, + wantErr: PUBLIC_DIR_ERROR_PREFIX, + }, + { + desc: "should error out if there is a public direcotry and an i18n with no root", + site: { + public: "public", + i18n: {} as unknown as { root: string }, + rewrites: [{ source: "/foo", function: "pass" }], + }, + wantErr: /Must supply a "root"/, + }, + { + desc: "should error out if region is set and function is unset", + site: { + rewrites: [{ source: "/", region: "us-central1" } as any], + }, + wantErr: + /Rewrites only support 'region' as a top-level field when 'function' is set as a string/, + }, + { + desc: "should error out if region is set and functions is the new form", + site: { + rewrites: [ + { + source: "/", + region: "us-central1", + function: { + functionId: "id", + }, + }, + ], + }, + wantErr: + /Rewrites only support 'region' as a top-level field when 'function' is set as a string/, + }, + { + desc: "should pass with public and nothing else", + site: { public: "public" }, + }, + { + desc: "should pass with no public but a function rewrite", + site: { + rewrites: [{ source: "/", function: "app" }], + }, + }, + { + desc: "should pass with no public but a run rewrite", + site: { + rewrites: [{ source: "/", run: { serviceId: "app" } }], + }, + }, + { + desc: "should pass with no public but a redirect", + site: { + redirects: [{ source: "/", destination: "https://google.com", type: 302 }], + }, + }, + ]; + + for (const t of tests) { + it(t.desc, () => { + // Setting experiment to "false" to handle mismatched error message. + setEnabled("webframeworks", false); + + const configs: HostingMultiple = [{ site: "site", ...t.site }]; + if (t.wantErr) { + expect(() => config.validate(configs, options(t.site))).to.throw( + FirebaseError, + t.wantErr, + ); + } else { + expect(() => config.validate(configs, options(t.site))).to.not.throw(); + } + }); + } + }); +}); diff --git a/src/hosting/config.ts b/src/hosting/config.ts new file mode 100644 index 00000000000..ffea0a0963d --- /dev/null +++ b/src/hosting/config.ts @@ -0,0 +1,312 @@ +import { bold } from "colorette"; +import { cloneDeep, logLabeledWarning } from "../utils"; + +import { FirebaseError } from "../error"; +import { + HostingMultiple, + HostingSingle, + HostingBase, + Deployable, + HostingRewrites, + FunctionsRewrite, + LegacyFunctionsRewrite, + HostingSource, +} from "../firebaseConfig"; +import { partition } from "../functional"; +import { dirExistsSync } from "../fsutils"; +import { resolveProjectPath } from "../projectPath"; +import { HostingOptions } from "./options"; +import * as path from "node:path"; +import { logger } from "../logger"; + +// After validating a HostingMultiple and resolving targets, we will instead +// have a HostingResolved. +export type HostingResolved = HostingBase & { + site: string; + target?: string; + webFramework?: string; +} & Deployable; + +// assertMatches allows us to throw when an --only flag doesn't match a target +// but an --except flag doesn't. Is this desirable behavior? +function matchingConfigs( + configs: HostingMultiple, + targets: string[], + assertMatches: boolean, +): HostingMultiple { + const matches: HostingMultiple = []; + const [hasSite, hasTarget] = partition(configs, (c) => "site" in c); + for (const target of targets) { + const siteMatch = hasSite.find((c) => c.site === target); + const targetMatch = hasTarget.find((c) => c.target === target); + if (siteMatch) { + matches.push(siteMatch); + } else if (targetMatch) { + matches.push(targetMatch); + } else if (assertMatches) { + throw new FirebaseError( + `Hosting site or target ${bold(target)} not detected in firebase.json`, + ); + } + } + return matches; +} + +/** + * Returns a subset of configs that match the only string + */ +export function filterOnly(configs: HostingMultiple, onlyString?: string): HostingMultiple { + if (!onlyString) { + return configs; + } + + let onlyTargets = onlyString.split(","); + // If an unqualified "hosting" is in the --only, + // all hosting sites should be deployed. + if (onlyTargets.includes("hosting")) { + return configs; + } + + // Strip out Hosting deploy targets from onlyTarget + onlyTargets = onlyTargets + .filter((target) => target.startsWith("hosting:")) + .map((target) => target.replace("hosting:", "")); + + return matchingConfigs(configs, onlyTargets, /* assertMatch= */ true); +} + +/** + * Returns a subset of configs that match the except string; + */ +export function filterExcept(configs: HostingMultiple, exceptOption?: string): HostingMultiple { + if (!exceptOption) { + return configs; + } + + const exceptTargets = exceptOption.split(","); + if (exceptTargets.includes("hosting")) { + return []; + } + + const exceptValues = exceptTargets + .filter((t) => t.startsWith("hosting:")) + .map((t) => t.replace("hosting:", "")); + const toReject = matchingConfigs(configs, exceptValues, /* assertMatch= */ false); + + return configs.filter((c) => !toReject.find((r) => c.site === r.site && c.target === r.target)); +} + +/** + * Verifies that input in firebase.json is sane + * @param options options from the command library + * @return a deep copy of validated configs + */ +export function extract(options: HostingOptions): HostingMultiple { + const config = options.config.src; + if (!config.hosting) { + return []; + } + const assertOneTarget = (config: HostingSingle): void => { + if (config.target && config.site) { + throw new FirebaseError( + `Hosting configs should only include either "site" or "target", not both.`, + ); + } + }; + + if (!Array.isArray(config.hosting)) { + // Upgrade the type because we pinky swear to ensure site exists as a backup. + const res = cloneDeep(config.hosting) as unknown as HostingMultiple[number]; + + // earlier the default RTDB instance was used as the hosting site + // because it used to be created along with the Firebase project. + // RTDB instance creation is now deferred and decoupled from project creation. + // the fallback hosting site is now filled in through requireHostingSite. + if (!res.target && !res.site) { + // Fun fact. Site can be the empty string if someone just downloads code + // and launches the emulator before configuring a project. + res.site = options.site; + } + assertOneTarget(res); + return [res]; + } else { + config.hosting.forEach(assertOneTarget); + return cloneDeep(config.hosting); + } +} + +/** Validates hosting configs for semantic correctness. */ +export function validate(configs: HostingMultiple, options: HostingOptions): void { + for (const config of configs) { + validateOne(config, options); + } +} + +function validateOne(config: HostingMultiple[number], options: HostingOptions): void { + // NOTE: a possible validation is to make sure site and target are not both + // specified, but this expectation is broken after calling resolveTargets. + // Thus that one validation is tucked into extract() where we know we haven't + // resolved targets yet. + + const hasAnyStaticRewrites = !!config.rewrites?.find((rw) => "destination" in rw); + const hasAnyDynamicRewrites = !!config.rewrites?.find((rw) => !("destination" in rw)); + const hasAnyRedirects = !!config.redirects?.length; + + if (config.source && config.public) { + throw new FirebaseError('Can only specify "source" or "public" in a Hosting config, not both'); + } + const root = config.source || config.public; + + if (!root && hasAnyStaticRewrites) { + throw new FirebaseError( + `Must supply a "public" or "source" directory when using "destination" rewrites.`, + ); + } + + if (!root && !hasAnyDynamicRewrites && !hasAnyRedirects) { + throw new FirebaseError( + `Must supply a "public" or "source" directory or at least one rewrite or redirect in each "hosting" config.`, + ); + } + + if (root && !dirExistsSync(resolveProjectPath(options, root))) { + logger.debug( + `Specified "${ + config.source ? "source" : "public" + }" directory "${root}" does not exist; Deploy to Hosting site "${ + config.site || config.target || "" + }" may fail or be empty.`, + ); + } + + // Using stupid types because type unions are painful sometimes + const regionWithoutFunction = (rewrite: Record): boolean => + typeof rewrite.region === "string" && typeof rewrite.function !== "string"; + const violation = config.rewrites?.find(regionWithoutFunction); + if (violation) { + throw new FirebaseError( + "Rewrites only support 'region' as a top-level field when 'function' is set as a string", + ); + } + + if (config.i18n) { + if (!root) { + throw new FirebaseError( + `Must supply a "public" or "source" directory when using "i18n" configuration.`, + ); + } + + if (!config.i18n.root) { + throw new FirebaseError('Must supply a "root" in "i18n" config.'); + } + + const i18nPath = path.join(root, config.i18n.root); + if (!dirExistsSync(resolveProjectPath(options, i18nPath))) { + logLabeledWarning( + "hosting", + `Couldn't find specified i18n root directory ${bold( + config.i18n.root, + )} in public directory ${bold(root)}`, + ); + } + } +} + +/** + * Converts all configs from having a target to having a source + */ +export function resolveTargets( + configs: HostingMultiple, + options: HostingOptions, +): HostingResolved[] { + return configs.map((config) => { + const newConfig = cloneDeep(config); + if (config.site) { + return newConfig as HostingResolved; + } + if (!config.target) { + throw new FirebaseError( + "Assertion failed: resolving hosting target of a site with no site name " + + "or target name. This should have caused an error earlier", + { exit: 2 }, + ); + } + if (!options.project) { + throw new FirebaseError( + "Assertion failed: options.project is not set. Commands depending on hosting.config should use requireProject", + { exit: 2 }, + ); + } + const matchingTargets = options.rc.requireTarget(options.project, "hosting", config.target); + if (matchingTargets.length > 1) { + throw new FirebaseError( + `Hosting target ${bold(config.target)} is linked to multiple sites, ` + + `but only one is permitted. ` + + `To clear, run:\n\n ${bold(`firebase target:clear hosting ${config.target}`)}`, + ); + } + newConfig.site = matchingTargets[0]; + return newConfig as HostingResolved; + }); +} + +function isLegacyFunctionsRewrite( + rewrite: HostingRewrites, +): rewrite is HostingSource & LegacyFunctionsRewrite { + return "function" in rewrite && typeof rewrite.function === "string"; +} + +/** + * Ensures that all configs are of a single modern format + */ +export function normalize(configs: HostingMultiple): void { + for (const config of configs) { + config.rewrites = config.rewrites?.map((rewrite) => { + if (!("function" in rewrite)) { + return rewrite; + } + if (isLegacyFunctionsRewrite(rewrite)) { + const modern: HostingRewrites & FunctionsRewrite = { + // Note: this copied in a bad "function" and "rewrite" in this splat + // we'll overwrite function and delete rewrite. + ...rewrite, + function: { + functionId: rewrite.function, + // Do not set pinTag so we can track how often it is used + }, + }; + delete (modern as unknown as LegacyFunctionsRewrite).region; + if ("region" in rewrite && typeof rewrite.region === "string") { + modern.function.region = rewrite.region; + } + if (rewrite.region) { + modern.function.region = rewrite.region; + } + return modern; + } + return rewrite; + }); + } +} + +/** + * Extract a validated normalized set of Hosting configs from the command options. + * This also resolves targets, so it is not suitable for the emulator. + */ +export function hostingConfig(options: HostingOptions): HostingResolved[] { + if (!options.normalizedHostingConfig) { + let configs: HostingMultiple = extract(options); + configs = filterOnly(configs, options.only); + configs = filterExcept(configs, options.except); + normalize(configs); + validate(configs, options); + + // N.B. We're calling resolveTargets after filterOnly/except, which means + // we won't recognize a --only when the config has a target. + // This is the way I found this code and should bring up to others whether + // we should change the behavior. + const resolved = resolveTargets(configs, options); + options.normalizedHostingConfig = resolved; + } + return options.normalizedHostingConfig; +} diff --git a/src/hosting/expireUtils.spec.ts b/src/hosting/expireUtils.spec.ts new file mode 100644 index 00000000000..7bf1087ba5a --- /dev/null +++ b/src/hosting/expireUtils.spec.ts @@ -0,0 +1,49 @@ +import { expect } from "chai"; + +import { calculateChannelExpireTTL } from "./expireUtils"; +import { FirebaseError } from "../error"; + +describe("calculateChannelExpireTTL", () => { + const goodTests = [ + { input: "30d", want: 30 * 24 * 60 * 60 * 1000 }, + { input: "1d", want: 24 * 60 * 60 * 1000 }, + { input: "2d", want: 2 * 24 * 60 * 60 * 1000 }, + { input: "2h", want: 2 * 60 * 60 * 1000 }, + { input: "56m", want: 56 * 60 * 1000 }, + ] as const; + + for (const test of goodTests) { + it(`should be able to parse time ${test.input}`, () => { + const got = calculateChannelExpireTTL(test.input); + expect(got).to.equal(test.want, `unexpected output for ${test.input}`); + }); + } + + const badTests = [ + { input: "1.5d" }, + { input: "2x" }, + { input: "2dd" }, + { input: "0.5m" }, + { input: undefined }, + ]; + + for (const test of badTests) { + it(`should be able to parse time ${test.input || "undefined"}`, () => { + expect(() => calculateChannelExpireTTL(test.input as any)).to.throw( + FirebaseError, + /flag must be a duration string/, + ); + }); + } + + it("should throw if greater than 30d", () => { + expect(() => calculateChannelExpireTTL("31d")).to.throw( + FirebaseError, + /not be longer than 30d/, + ); + expect(() => calculateChannelExpireTTL(`${31 * 24}h`)).to.throw( + FirebaseError, + /not be longer than 30d/, + ); + }); +}); diff --git a/src/hosting/expireUtils.ts b/src/hosting/expireUtils.ts index 390344bb742..a33ed809d79 100644 --- a/src/hosting/expireUtils.ts +++ b/src/hosting/expireUtils.ts @@ -1,4 +1,5 @@ import { FirebaseError } from "../error"; +import { HostingOptions } from "./options"; /** * A regex to test for valid duration strings. @@ -36,11 +37,11 @@ export const DEFAULT_DURATION = 7 * Duration.DAY; * @param flag string duration (e.g. "1d"). * @return a duration in milliseconds. */ -export function calculateChannelExpireTTL(flag?: string): number { - const match = DURATION_REGEX.exec(flag || ""); +export function calculateChannelExpireTTL(flag: NonNullable): number { + const match = DURATION_REGEX.exec(flag); if (!match) { throw new FirebaseError( - `"expires" flag must be a duration string (e.g. 24h or 7d) at most 30d` + `"expires" flag must be a duration string (e.g. 24h or 7d) at most 30d`, ); } const d = parseInt(match[1], 10) * DURATIONS[match[2]]; diff --git a/src/test/hosting/functionsProxy.spec.ts b/src/hosting/functionsProxy.spec.ts similarity index 82% rename from src/test/hosting/functionsProxy.spec.ts rename to src/hosting/functionsProxy.spec.ts index 948d1763791..82a4ac40674 100644 --- a/src/test/hosting/functionsProxy.spec.ts +++ b/src/hosting/functionsProxy.spec.ts @@ -5,13 +5,11 @@ import * as nock from "nock"; import * as sinon from "sinon"; import * as supertest from "supertest"; -import functionsProxy, { - FunctionProxyRewrite, - FunctionsProxyOptions, -} from "../../hosting/functionsProxy"; -import { EmulatorRegistry } from "../../emulator/registry"; -import { Emulators } from "../../emulator/types"; -import { FakeEmulator } from "../emulators/fakeEmulator"; +import { functionsProxy, FunctionsProxyOptions } from "./functionsProxy"; +import { EmulatorRegistry } from "../emulator/registry"; +import { Emulators } from "../emulator/types"; +import { FakeEmulator } from "../emulator/testing/fakeEmulator"; +import { HostingRewrites } from "../firebaseConfig"; describe("functionsProxy", () => { const fakeOptions: FunctionsProxyOptions = { @@ -20,10 +18,16 @@ describe("functionsProxy", () => { targets: [], }; - const fakeRewrite: FunctionProxyRewrite = { function: "bar" }; + const fakeRewrite = { function: "bar", region: "us-central1" } as HostingRewrites; + const fakeRewriteEurope = { + function: "bar", + region: "europe-west3", + } as HostingRewrites; beforeEach(async () => { - const fakeFunctionsEmulator = new FakeEmulator(Emulators.FUNCTIONS, "localhost", 7778); + const fakeFunctionsEmulator = new FakeEmulator(Emulators.FUNCTIONS, [ + { address: "127.0.0.1", family: "IPv4", port: 7778 }, + ]); await EmulatorRegistry.start(fakeFunctionsEmulator); }); @@ -49,8 +53,25 @@ describe("functionsProxy", () => { }); }); + it("should resolve a function returns middleware that proxies to the live version in another region", async () => { + nock("https://europe-west3-project-foo.cloudfunctions.net") + .get("/bar/") + .reply(200, "live version"); + + const mwGenerator = functionsProxy(fakeOptions); + const mw = await mwGenerator(fakeRewriteEurope); + const spyMw = sinon.spy(mw); + + return supertest(spyMw) + .get("/") + .expect(200, "live version") + .then(() => { + expect(spyMw.calledOnce).to.be.true; + }); + }); + it("should resolve a function that returns middleware that proxies to a local version", async () => { - nock("http://localhost:7778").get("/project-foo/us-central1/bar/").reply(200, "local version"); + nock("http://127.0.0.1:7778").get("/project-foo/us-central1/bar/").reply(200, "local version"); const options = cloneDeep(fakeOptions); options.targets = ["functions"]; @@ -67,8 +88,26 @@ describe("functionsProxy", () => { }); }); + it("should resolve a function that returns middleware that proxies to a local version in another region", async () => { + nock("http://127.0.0.1:7778").get("/project-foo/europe-west3/bar/").reply(200, "local version"); + + const options = cloneDeep(fakeOptions); + options.targets = ["functions"]; + + const mwGenerator = functionsProxy(options); + const mw = await mwGenerator(fakeRewriteEurope); + const spyMw = sinon.spy(mw); + + return supertest(spyMw) + .get("/") + .expect(200, "local version") + .then(() => { + expect(spyMw.calledOnce).to.be.true; + }); + }); + it("should maintain the location header as returned by the function", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(301, "", { location: "/over-here" }); @@ -89,7 +128,7 @@ describe("functionsProxy", () => { }); it("should allow location headers that wouldn't redirect to itself", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(301, "", { location: "https://example.com/foo" }); @@ -110,7 +149,7 @@ describe("functionsProxy", () => { }); it("should proxy a request body on a POST request", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .post("/project-foo/us-central1/bar/", "data") .reply(200, "you got post data"); @@ -131,7 +170,7 @@ describe("functionsProxy", () => { }); it("should proxy with a query string", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .query({ key: "value" }) .reply(200, "query!"); @@ -153,7 +192,7 @@ describe("functionsProxy", () => { }); it("should return 3xx responses directly", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(301, "redirected", { Location: "https://example.com" }); @@ -173,7 +212,7 @@ describe("functionsProxy", () => { }); it("should pass through multiple set-cookie headers", async () => { - nock("http://localhost:7778") + nock("http://127.0.0.1:7778") .get("/project-foo/us-central1/bar/") .reply(200, "crisp", { "Set-Cookie": ["foo=bar", "bar=zap"], diff --git a/src/hosting/functionsProxy.ts b/src/hosting/functionsProxy.ts index 582790131a4..6f78e95e990 100644 --- a/src/hosting/functionsProxy.ts +++ b/src/hosting/functionsProxy.ts @@ -6,6 +6,8 @@ import { needProjectId } from "../projectUtils"; import { EmulatorRegistry } from "../emulator/registry"; import { Emulators } from "../emulator/types"; import { FunctionsEmulator } from "../emulator/functionsEmulator"; +import { HostingRewrites, LegacyFunctionsRewrite } from "../firebaseConfig"; +import { FirebaseError } from "../error"; export interface FunctionsProxyOptions { port: number; @@ -13,24 +15,32 @@ export interface FunctionsProxyOptions { targets: string[]; } -export interface FunctionProxyRewrite { - function: string; -} - /** * Returns a function which, given a FunctionProxyRewrite, returns a Promise * that resolves with a middleware-like function that proxies the request to a * hosted or live function. */ -export default function ( - options: FunctionsProxyOptions -): (r: FunctionProxyRewrite) => Promise { - return (rewrite: FunctionProxyRewrite) => { +export function functionsProxy( + options: FunctionsProxyOptions, +): (r: HostingRewrites) => Promise { + return (rewrite: HostingRewrites) => { return new Promise((resolve) => { - // TODO(samstern): This proxy assumes all functions are in the default region, but this is - // not a safe assumption. const projectId = needProjectId(options); - let url = `https://us-central1-${projectId}.cloudfunctions.net/${rewrite.function}`; + if (!("function" in rewrite)) { + throw new FirebaseError(`A non-function rewrite cannot be used in functionsProxy`, { + exit: 2, + }); + } + let functionId: string; + let region: string; + if (typeof rewrite.function === "string") { + functionId = rewrite.function; + region = (rewrite as LegacyFunctionsRewrite).region || "us-central1"; + } else { + functionId = rewrite.function.functionId; + region = rewrite.function.region || "us-central1"; + } + let url = `https://${region}-${projectId}.cloudfunctions.net/${functionId}`; let destLabel = "live"; if (includes(options.targets, "functions")) { @@ -38,19 +48,12 @@ export default function ( // If the functions emulator is running we know the port, otherwise // things still point to production. - const functionsEmu = EmulatorRegistry.get(Emulators.FUNCTIONS); - if (functionsEmu) { - url = FunctionsEmulator.getHttpFunctionUrl( - functionsEmu.getInfo().host, - functionsEmu.getInfo().port, - projectId, - rewrite.function, - "us-central1" - ); + if (EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { + url = FunctionsEmulator.getHttpFunctionUrl(projectId, functionId, region); } } - resolve(proxyRequestHandler(url, `${destLabel} Function ${rewrite.function}`)); + resolve(proxyRequestHandler(url, `${destLabel} Function ${region}/${functionId}`)); }); }; } diff --git a/src/hosting/implicitInit.ts b/src/hosting/implicitInit.ts index 242cbac4304..6ee0caf8cc9 100644 --- a/src/hosting/implicitInit.ts +++ b/src/hosting/implicitInit.ts @@ -1,14 +1,14 @@ import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as fs from "fs"; +import * as clc from "colorette"; import { fetchWebSetup, getCachedWebSetup } from "../fetchWebSetup"; import * as utils from "../utils"; import { logger } from "../logger"; import { EmulatorRegistry } from "../emulator/registry"; -import { EMULATORS_SUPPORTED_BY_USE_EMULATOR, Address, Emulators } from "../emulator/types"; +import { EMULATORS_SUPPORTED_BY_USE_EMULATOR, Emulators } from "../emulator/types"; +import { readTemplateSync } from "../templates"; -const INIT_TEMPLATE = fs.readFileSync(__dirname + "/../../templates/hosting/init.js", "utf8"); +const INIT_TEMPLATE = readTemplateSync("hosting/init.js"); export interface TemplateServerResponse { // __init.js content with only initializeApp() @@ -18,7 +18,7 @@ export interface TemplateServerResponse { emulatorsJs: string; // firebaseConfig JSON - json: string; + json?: string; } /** @@ -31,15 +31,15 @@ export async function implicitInit(options: any): Promise { beforeEach(async () => { port = await portfinder.getPortPromise(); - await new Promise((resolve) => (server = app.listen(port, resolve))); + await new Promise((resolve) => (server = app.listen(port, resolve))); }); afterEach(async () => { @@ -163,7 +163,7 @@ describe("initMiddleware", () => { port, path: `/__/firebase/v2.2.2/sample-sdk.js`, }, - resolve + resolve, ); req.on("error", reject); req.end(); diff --git a/src/hosting/initMiddleware.ts b/src/hosting/initMiddleware.ts index b70a3cf1bec..d2ae03625c1 100644 --- a/src/hosting/initMiddleware.ts +++ b/src/hosting/initMiddleware.ts @@ -1,6 +1,4 @@ -import * as url from "url"; -import * as qs from "querystring"; -import { RequestHandler } from "express"; +import { IncomingMessage, ServerResponse } from "http"; import { Client } from "../apiv2"; import { TemplateServerResponse } from "./implicitInit"; @@ -15,14 +13,16 @@ const SDK_PATH_REGEXP = /^\/__\/firebase\/([^/]+)\/([^/]+)$/; * @param init template server response. * @return the middleware function. */ -export function initMiddleware(init: TemplateServerResponse): RequestHandler { +export function initMiddleware( + init: TemplateServerResponse, +): (req: IncomingMessage, res: ServerResponse, next: () => void) => void { return (req, res, next) => { - const parsedUrl = url.parse(req.url); - const match = RegExp(SDK_PATH_REGEXP).exec(req.url); + const parsedUrl = new URL(req.url || "", `http://${req.headers.host}`); + const match = RegExp(SDK_PATH_REGEXP).exec(parsedUrl.pathname); if (match) { const version = match[1]; const sdkName = match[2]; - const u = new url.URL(`https://www.gstatic.com/firebasejs/${version}/${sdkName}`); + const u = new URL(`https://www.gstatic.com/firebasejs/${version}/${sdkName}`); const c = new Client({ urlPrefix: u.origin, auth: false }); const headers: { [key: string]: string } = {}; const acceptEncoding = req.headers["accept-encoding"]; @@ -49,7 +49,7 @@ export function initMiddleware(init: TemplateServerResponse): RequestHandler { .catch((e) => { utils.logLabeledWarning( "hosting", - `Could not load Firebase SDK ${sdkName} v${version}, check your internet connection.` + `Could not load Firebase SDK ${sdkName} v${version}, check your internet connection.`, ); logger.debug(e); }); @@ -57,10 +57,9 @@ export function initMiddleware(init: TemplateServerResponse): RequestHandler { // In theory we should be able to get this from req.query but for some // when testing this functionality, req.query and req.params were always // empty or undefined. - const query = qs.parse(parsedUrl.query || ""); - + const query = parsedUrl.searchParams; res.setHeader("Content-Type", "application/javascript"); - if (query["useEmulator"] === "true") { + if (query.get("useEmulator") === "true") { res.end(init.emulatorsJs); } else { res.end(init.js); diff --git a/src/hosting/interactive.ts b/src/hosting/interactive.ts new file mode 100644 index 00000000000..0c60d0088e6 --- /dev/null +++ b/src/hosting/interactive.ts @@ -0,0 +1,92 @@ +import { FirebaseError } from "../error"; +import { logWarning } from "../utils"; +import { needProjectId, needProjectNumber } from "../projectUtils"; +import { promptOnce } from "../prompt"; +import { Site, createSite } from "./api"; + +const nameSuggestion = new RegExp("try something like `(.+)`"); +// const prompt = "Please provide an unique, URL-friendly id for the site (.web.app):"; +const prompt = + "Please provide an unique, URL-friendly id for your site. Your site's URL will be .web.app. " + + 'We recommend using letters, numbers, and hyphens (e.g. "{project-id}-{random-hash}"):'; + +/** + * Interactively prompt to create a Hosting site. + */ +export async function interactiveCreateHostingSite( + siteId: string, + appId: string, + options: { projectId?: string; nonInteractive?: boolean }, +): Promise { + const projectId = needProjectId(options); + const projectNumber = await needProjectNumber(options); + let id = siteId; + let newSite: Site | undefined; + let suggestion: string | undefined; + + // If we were given an ID, we're going to start with that, so don't check the project ID. + // If we weren't given an ID, let's _suggest_ the project ID as the site name (or a variant). + if (!id) { + const attempt = await trySiteID(projectNumber, projectId); + if (attempt.available) { + suggestion = projectId; + } else { + suggestion = attempt.suggestion; + } + } + + while (!newSite) { + if (!id || suggestion) { + id = await promptOnce({ + type: "input", + message: prompt, + validate: (s: string) => s.length > 0, // Prevents an empty string from being submitted! + default: suggestion, + }); + } + try { + newSite = await createSite(projectNumber, id, appId); + } catch (err: unknown) { + if (!(err instanceof FirebaseError)) { + throw err; + } + if (options.nonInteractive) { + throw err; + } + + id = ""; // Clear so the prompt comes back. + suggestion = getSuggestionFromError(err); + } + } + return newSite; +} + +async function trySiteID( + projectNumber: string, + id: string, +): Promise<{ available: boolean; suggestion?: string }> { + try { + await createSite(projectNumber, id, "", true); + return { available: true }; + } catch (err: unknown) { + if (!(err instanceof FirebaseError)) { + throw err; + } + const suggestion = getSuggestionFromError(err); + return { available: false, suggestion }; + } +} + +function getSuggestionFromError(err: FirebaseError): string | undefined { + if (err.status === 400 && err.message.includes("Invalid name:")) { + const i = err.message.indexOf("Invalid name:"); + logWarning(err.message.substring(i)); + const match = nameSuggestion.exec(err.message); + if (match) { + return match[1]; + } + } else { + logWarning(err.message); + } + return; +} diff --git a/src/hosting/normalizedHostingConfigs.ts b/src/hosting/normalizedHostingConfigs.ts deleted file mode 100644 index f2833e06694..00000000000 --- a/src/hosting/normalizedHostingConfigs.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { bold } from "cli-color"; -import { cloneDeep } from "lodash"; - -import { FirebaseError } from "../error"; - -interface HostingConfig { - site: string; - target: string; -} - -function filterOnly(configs: HostingConfig[], onlyString: string): HostingConfig[] { - if (!onlyString) { - return configs; - } - - let onlyTargets = onlyString.split(","); - // If an unqualified "hosting" is in the --only, - // all hosting sites should be deployed. - if (onlyTargets.includes("hosting")) { - return configs; - } - - // Strip out Hosting deploy targets from onlyTarget - onlyTargets = onlyTargets - .filter((target) => target.startsWith("hosting:")) - .map((target) => target.replace("hosting:", "")); - - const configsBySite = new Map(); - const configsByTarget = new Map(); - for (const c of configs) { - if (c.site) { - configsBySite.set(c.site, c); - } - if (c.target) { - configsByTarget.set(c.target, c); - } - } - - const filteredConfigs: HostingConfig[] = []; - // Check to see that all the hosting deploy targets exist in the hosting - // config as either `site`s or `target`s. - for (const onlyTarget of onlyTargets) { - if (configsBySite.has(onlyTarget)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - filteredConfigs.push(configsBySite.get(onlyTarget)!); - } else if (configsByTarget.has(onlyTarget)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - filteredConfigs.push(configsByTarget.get(onlyTarget)!); - } else { - throw new FirebaseError( - `Hosting site or target ${bold(onlyTarget)} not detected in firebase.json` - ); - } - } - - return filteredConfigs; -} - -function filterExcept(configs: HostingConfig[], exceptOption: string): HostingConfig[] { - if (!exceptOption) { - return configs; - } - - const exceptTargets = exceptOption.split(","); - if (exceptTargets.includes("hosting")) { - return []; - } - - const exceptValues = new Set( - exceptTargets.filter((t) => t.startsWith("hosting:")).map((t) => t.replace("hosting:", "")) - ); - - const filteredConfigs: HostingConfig[] = []; - for (const c of configs) { - if (!(exceptValues.has(c.site) || exceptValues.has(c.target))) { - filteredConfigs.push(c); - } - } - - return filteredConfigs; -} - -/** - * Normalize options to HostingConfig array. - * @param cmdOptions the Firebase CLI options object. - * @param options options for normalizing configs. - * @return normalized hosting config array. - */ -export function normalizedHostingConfigs( - cmdOptions: any, // eslint-disable-line @typescript-eslint/no-explicit-any - options: { resolveTargets?: boolean } = {} -): HostingConfig[] { - let configs = cloneDeep(cmdOptions.config.get("hosting")); - if (!configs) { - return []; - } - if (!Array.isArray(configs)) { - if (!configs.target && !configs.site) { - // earlier the default RTDB instance was used as the hosting site - // because it used to be created along with the Firebase project. - // RTDB instance creation is now deferred and decoupled from project creation. - // the fallback hosting site is now filled in through requireHostingSite. - configs.site = cmdOptions.site; - } - configs = [configs]; - } - - for (const c of configs) { - if (c.target && c.site) { - throw new FirebaseError( - `Hosting configs should only include either "site" or "target", not both.` - ); - } - } - - // filter* functions check if the strings are empty for us. - let hostingConfigs: HostingConfig[] = filterOnly(configs, cmdOptions.only); - hostingConfigs = filterExcept(hostingConfigs, cmdOptions.except); - - if (options.resolveTargets) { - for (const cfg of hostingConfigs) { - if (cfg.target) { - const matchingTargets = cmdOptions.rc.requireTarget( - cmdOptions.project, - "hosting", - cfg.target - ); - if (matchingTargets.length > 1) { - throw new FirebaseError( - `Hosting target ${bold(cfg.target)} is linked to multiple sites, ` + - `but only one is permitted. ` + - `To clear, run:\n\n firebase target:clear hosting ${cfg.target}` - ); - } - cfg.site = matchingTargets[0]; - } else if (!cfg.site) { - throw new FirebaseError('Must supply either "site" or "target" in each "hosting" config.'); - } - } - } - - return hostingConfigs; -} diff --git a/src/hosting/options.ts b/src/hosting/options.ts new file mode 100644 index 00000000000..e7c4d7e368e --- /dev/null +++ b/src/hosting/options.ts @@ -0,0 +1,30 @@ +import { FirebaseConfig } from "../firebaseConfig"; +import { assertImplements } from "../metaprogramming"; +import { Options } from "../options"; +import { HostingResolved } from "./config"; + +/** + * The set of fields that the Hosting codebase needs from Options. + * It is preferable that all codebases use this technique so that they keep + * strong typing in their codebase but limit the codebase to have less to mock. + */ +export interface HostingOptions { + project?: string; + site?: string; + config: { + src: FirebaseConfig; + }; + rc: { + requireTarget(project: string, type: string, name: string): string[]; + }; + cwd?: string; + configPath?: string; + only?: string; + except?: string; + normalizedHostingConfig?: Array; + expires?: `${number}${"h" | "d" | "m"}`; +} + +// This line caues a compile-time error if HostingOptions has a field that is +// missing in Options or incompatible with the type in Options. +assertImplements(); diff --git a/src/hosting/proxy.ts b/src/hosting/proxy.ts index eeedfce5750..11f619dfce7 100644 --- a/src/hosting/proxy.ts +++ b/src/hosting/proxy.ts @@ -39,8 +39,12 @@ function makeVary(vary: string | null = ""): string { * cookies, and caching similar to the behavior of the production version of * the Firebase Hosting origin. */ -export function proxyRequestHandler(url: string, rewriteIdentifier: string): RequestHandler { - return async (req: IncomingMessage, res: ServerResponse, next: () => void): Promise => { +export function proxyRequestHandler( + url: string, + rewriteIdentifier: string, + options: { forceCascade?: boolean } = {}, +): RequestHandler { + return async (req: IncomingMessage, res: ServerResponse, next: () => void): Promise => { logger.info(`[hosting] Rewriting ${req.url} to ${url} for ${rewriteIdentifier}`); // Extract the __session cookie from headers to forward it to the // functions cookie is not a string[]. @@ -75,7 +79,7 @@ export function proxyRequestHandler(url: string, rewriteIdentifier: string): Req continue; } const value = req.headers[key]; - if (value == undefined) { + if (value === undefined) { headers.delete(key); } else if (Array.isArray(value)) { headers.delete(key); @@ -101,7 +105,7 @@ export function proxyRequestHandler(url: string, rewriteIdentifier: string): Req timeout: 60000, compress: false, }); - } catch (err) { + } catch (err: any) { const isAbortError = err instanceof FirebaseError && err.original?.name.includes("AbortError"); const isTimeoutError = @@ -123,7 +127,7 @@ export function proxyRequestHandler(url: string, rewriteIdentifier: string): Req if (proxyRes.status === 404) { // x-cascade is not a string[]. const cascade = proxyRes.response.headers.get("x-cascade"); - if (cascade && cascade.toUpperCase() === "PASS") { + if (options.forceCascade || (cascade && cascade.toUpperCase() === "PASS")) { return next(); } } @@ -155,13 +159,13 @@ export function proxyRequestHandler(url: string, rewriteIdentifier: string): Req const locationURL = new URL(location); // Only assume we can fix the location header if the origin of the // "fixed" header is the same as the origin of the outbound request. - if (locationURL.origin == u.origin) { + if (locationURL.origin === u.origin) { const unborkedLocation = location.replace(locationURL.origin, ""); proxyRes.response.headers.set("location", unborkedLocation); } - } catch (e) { + } catch (e: any) { logger.debug( - `[hosting] had trouble parsing location header, but this may be okay: "${location}"` + `[hosting] had trouble parsing location header, but this may be okay: "${location}"`, ); } } @@ -179,7 +183,7 @@ export function proxyRequestHandler(url: string, rewriteIdentifier: string): Req * return an internal HTTP error response. */ export function errorRequestHandler(error: string): RequestHandler { - return (req: Request, res: Response, next: () => void): any => { + return (req: Request, res: Response): any => { res.statusCode = 500; const out = `A problem occurred while trying to handle a proxied rewrite: ${error}`; logger.error(out); diff --git a/src/hosting/runTags.spec.ts b/src/hosting/runTags.spec.ts new file mode 100644 index 00000000000..621aa061c43 --- /dev/null +++ b/src/hosting/runTags.spec.ts @@ -0,0 +1,307 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as runNS from "../gcp/run"; +import * as hostingNS from "./api"; +import * as runTagsNS from "./runTags"; +import { cloneDeep } from "../utils"; + +const REGION = "REGION"; +const SERVICE = "SERVICE"; +const PROJECT = "PROJECT"; + +describe("runTags", () => { + let run: sinon.SinonStubbedInstance; + let hosting: sinon.SinonStubbedInstance; + let runTags: sinon.SinonStubbedInstance; + const site: hostingNS.Site = { + name: "projects/project/sites/site", + defaultUrl: "https://google.com", + appId: "appId", + labels: {}, + }; + + function version( + version: string, + status: hostingNS.VersionStatus, + ...rewrites: hostingNS.RunRewrite[] + ): hostingNS.Version { + return { + name: `projects/project/sites/site/versions/${version}`, + status: status, + config: { + rewrites: rewrites.map((r) => { + return { regex: ".*", run: r }; + }), + }, + createTime: "now", + createUser: { + email: "inlined@gmail.com", + }, + fileCount: 0, + versionBytes: 0, + }; + } + + function service(id: string, ...tags: Array): runNS.Service { + return { + apiVersion: "serving.knative.dev/v1", + kind: "Service", + metadata: { + name: id, + namespace: PROJECT, + labels: { + [runNS.LOCATION_LABEL]: REGION, + }, + }, + spec: { + template: { + metadata: { + name: "revision", + namespace: "project", + }, + spec: { + containers: [], + }, + }, + traffic: [ + { + latestRevision: true, + percent: 100, + }, + ...tags.map((tag) => { + if (typeof tag === "string") { + return { + revisionName: `revision-${tag}`, + tag: tag, + percent: 0, + }; + } else { + return tag; + } + }), + ], + }, + status: { + observedGeneration: 50, + latestCreatedRevisionName: "latest", + latestReadyRevisionName: "latest", + traffic: [ + { + revisionName: "latest", + latestRevision: true, + percent: 100, + }, + ...tags.map((tag) => { + if (typeof tag === "string") { + return { + revisionName: `revision-${tag}`, + tag: tag, + percent: 0, + }; + } else { + return { + percent: 0, + ...tag, + }; + } + }), + ], + conditions: [], + url: "https://google.com", + address: { + url: "https://google.com", + }, + }, + }; + } + + beforeEach(() => { + // We need the library to attempt to do something for us to observe side effects. + run = sinon.stub(runNS); + hosting = sinon.stub(hostingNS); + runTags = sinon.stub(runTagsNS); + + hosting.listSites.withArgs(PROJECT).resolves([site]); + hosting.listVersions.rejects(new Error("Unexpected hosting.listSites")); + + run.getService.rejects(new Error("Unexpected run.getService")); + run.updateService.rejects(new Error("Unexpected run.updateService")); + run.gcpIds.restore(); + + runTags.ensureLatestRevisionTagged.throws( + new Error("Unexpected runTags.ensureLatestRevisionTagged"), + ); + runTags.gcTagsForServices.rejects(new Error("Unepxected runTags.gcTagsForServices")); + runTags.setRewriteTags.rejects(new Error("Unexpected runTags.setRewriteTags call")); + runTags.setGarbageCollectionThreshold.restore(); + }); + + afterEach(() => { + sinon.restore(); + }); + + function tagsIn(service: runNS.Service): string[] { + return service.spec.traffic.map((t) => t.tag).filter((t) => !!t) as string[]; + } + + describe("gcTagsForServices", () => { + beforeEach(() => { + runTags.gcTagsForServices.restore(); + }); + + it("leaves only active revisions", async () => { + hosting.listVersions.resolves([ + version("v1", "FINALIZED", { serviceId: "s1", region: REGION, tag: "fh-in-use1" }), + version("v2", "CREATED", { serviceId: "s1", region: REGION, tag: "fh-in-use2" }), + version("v3", "DELETED", { serviceId: "s1", region: REGION, tag: "fh-deleted-version" }), + ]); + + const s1 = service( + "s1", + "fh-in-use1", + "fh-in-use2", + "fh-deleted-version", + "fh-no-longer-referenced", + "not-by-us", + ); + const s2 = service("s2", "fh-no-reference"); + s2.spec.traffic.push({ + revisionName: "manual-split", + tag: "fh-manual-split", + percent: 1, + }); + await runTags.gcTagsForServices(PROJECT, [s1, s2]); + + expect(tagsIn(s1)).to.deep.equal(["fh-in-use1", "fh-in-use2", "not-by-us"]); + expect(tagsIn(s2)).to.deep.equal(["fh-manual-split"]); + }); + }); + + describe("setRewriteTags", () => { + const svc = service(SERVICE); + const svcName = `projects/${PROJECT}/locations/${REGION}/services/${SERVICE}`; + beforeEach(() => { + runTags.setRewriteTags.restore(); + }); + + it("preserves existing tags and other types of rewrites", async () => { + const rewrites: hostingNS.Rewrite[] = [ + { + glob: "**", + path: "/index.html", + }, + { + glob: "/dynamic", + run: { + serviceId: "service", + region: "us-central1", + tag: "someone-is-using-this-code-in-a-way-i-dont-expect", + }, + }, + { + glob: "/callable", + function: "function", + functionRegion: "us-central1", + }, + ]; + const original = cloneDeep(rewrites); + await runTags.setRewriteTags(rewrites, "project", "version"); + expect(rewrites).to.deep.equal(original); + }); + + it("replaces tags in rewrites with new/verified tags", async () => { + const rewrites: hostingNS.Rewrite[] = [ + { + glob: "**", + run: { + serviceId: SERVICE, + region: REGION, + tag: runTagsNS.TODO_TAG_NAME, + }, + }, + ]; + + run.getService.withArgs(svcName).resolves(svc); + // Calls fake apparently doesn't trum the default rejects command + runTags.ensureLatestRevisionTagged.resetBehavior(); + runTags.ensureLatestRevisionTagged.callsFake( + (svc: runNS.Service[], tag: string): Promise>> => { + expect(tag).to.equal("fh-version"); + svc[0].spec.traffic.push({ revisionName: "latest", tag }); + return Promise.resolve({ [REGION]: { [SERVICE]: tag } }); + }, + ); + + await runTags.setRewriteTags(rewrites, PROJECT, "version"); + expect(rewrites).to.deep.equal([ + { + glob: "**", + run: { + serviceId: SERVICE, + region: REGION, + tag: "fh-version", + }, + }, + ]); + }); + + it("garbage collects if necessary", async () => { + runTagsNS.setGarbageCollectionThreshold(2); + const svc = service(SERVICE, "fh-1", "fh-2"); + const rewrites: hostingNS.Rewrite[] = [ + { + glob: "**", + run: { + serviceId: SERVICE, + region: REGION, + tag: runTagsNS.TODO_TAG_NAME, + }, + }, + ]; + run.getService.withArgs(svcName).resolves(svc); + runTags.gcTagsForServices.resolves(); + runTags.ensureLatestRevisionTagged.resolves({ [REGION]: { [SERVICE]: "fh-3" } }); + await runTags.setRewriteTags(rewrites, PROJECT, "3"); + expect(runTags.ensureLatestRevisionTagged); + expect(runTags.gcTagsForServices).to.have.been.called; + }); + }); + + describe("ensureLatestRevisionTagged", () => { + beforeEach(() => { + runTags.ensureLatestRevisionTagged.restore(); + }); + + it("Reuses existing tag names", async () => { + const svc = service(SERVICE, { revisionName: "latest", tag: "existing" }); + await runTags.ensureLatestRevisionTagged([svc], "new-tag"); + expect(svc.spec.traffic).to.deep.equal([ + { + latestRevision: true, + percent: 100, + }, + { + revisionName: "latest", + tag: "existing", + }, + ]); + expect(run.updateService).to.not.have.been.called; + }); + + it("Adds new tags as necessary", async () => { + const svc = service(SERVICE); + run.updateService.resolves(); + await runTags.ensureLatestRevisionTagged([svc], "new-tag"); + expect(svc.spec.traffic).to.deep.equal([ + { + latestRevision: true, + percent: 100, + }, + { + revisionName: "latest", + tag: "new-tag", + }, + ]); + }); + }); +}); diff --git a/src/hosting/runTags.ts b/src/hosting/runTags.ts new file mode 100644 index 00000000000..4ada8359e91 --- /dev/null +++ b/src/hosting/runTags.ts @@ -0,0 +1,184 @@ +import { posix } from "node:path"; +import * as run from "../gcp/run"; +import * as api from "./api"; +import { FirebaseError } from "../error"; +import { flattenArray } from "../functional"; + +/** + * Sentinel to be used when creating an api.Rewrite with the tag option but + * you don't yet know the tag. Resolve this tag by passing the rewrite into + * setRewriteTags + */ +export const TODO_TAG_NAME = "this is an invalid tag name so it cannot be real"; + +/** + * Looks up all valid Hosting tags in this project and removes traffic targets + * from passed in services that don't match a valid tag. + * This makes no actual server-side changes to these services; you must then + * call run.updateService to save these changes. We divide this responsiblity + * because we want to possibly insert a new tagged target before saving. + */ +export async function gcTagsForServices(project: string, services: run.Service[]): Promise { + // region -> service -> tags + // We cannot simplify this into a single map because we might be mixing project + // id and number. + const validTagsByServiceByRegion: Record>> = {}; + const sites = await api.listSites(project); + const allVersionsNested = await Promise.all( + sites.map((site) => api.listVersions(posix.basename(site.name))), + ); + const activeVersions = [...flattenArray(allVersionsNested)].filter((version) => { + return version.status === "CREATED" || version.status === "FINALIZED"; + }); + for (const version of activeVersions) { + for (const rewrite of version?.config?.rewrites || []) { + if (!("run" in rewrite) || !rewrite.run.tag) { + continue; + } + validTagsByServiceByRegion[rewrite.run.region] = + validTagsByServiceByRegion[rewrite.run.region] || {}; + validTagsByServiceByRegion[rewrite.run.region][rewrite.run.serviceId] = + validTagsByServiceByRegion[rewrite.run.region][rewrite.run.serviceId] || new Set(); + validTagsByServiceByRegion[rewrite.run.region][rewrite.run.serviceId].add(rewrite.run.tag); + } + } + + // Erase all traffic targets that have an expired tag and no serving percentage + for (const service of services) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const { region, serviceId } = run.gcpIds(service); + service.spec.traffic = (service.spec.traffic || []).filter((traffic) => { + // If we're serving traffic irrespective of the tag, leave this target + if (traffic.percent) { + return true; + } + // Only GC targets with tags + if (!traffic.tag) { + return true; + } + // Only GC targets with tags that look like we added them + if (!traffic.tag.startsWith("fh-")) { + return true; + } + if (validTagsByServiceByRegion[region]?.[serviceId]?.has(traffic.tag)) { + return true; + } + return false; + }); + } +} + +// The number of tags after which we start applying GC pressure. +let garbageCollectionThreshold = 500; + +/** + * Sets the garbage collection threshold for testing. + * @param threshold new GC threshold. + */ +export function setGarbageCollectionThreshold(threshold: number): void { + garbageCollectionThreshold = threshold; +} + +/** + * Ensures that all the listed run versions have pins. + */ +export async function setRewriteTags( + rewrites: api.Rewrite[], + project: string, + version: string, +): Promise { + // Note: this is sub-optimal in the case where there are multiple rewrites + // to the same service. Should we deduplicate this? + const services: run.Service[] = await Promise.all( + rewrites + .map((rewrite) => { + if (!("run" in rewrite)) { + return null; + } + if (rewrite.run.tag !== TODO_TAG_NAME) { + return null; + } + + return run.getService( + `projects/${project}/locations/${rewrite.run.region}/services/${rewrite.run.serviceId}`, + ); + }) + // filter does not drop the null annotation + .filter((s) => s !== null) as Array>, + ); + // Unnecessary due to functional programming, but creates an observable side effect for tests + if (!services.length) { + return; + } + + const needsGC = services + .map((service) => { + return service.spec.traffic.filter((traffic) => traffic.tag).length; + }) + .some((length) => length >= garbageCollectionThreshold); + if (needsGC) { + await exports.gcTagsForServices(project, services); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const tags: Record> = await exports.ensureLatestRevisionTagged( + services, + `fh-${version}`, + ); + for (const rewrite of rewrites) { + if (!("run" in rewrite) || rewrite.run.tag !== TODO_TAG_NAME) { + continue; + } + const tag = tags[rewrite.run.region][rewrite.run.serviceId]; + rewrite.run.tag = tag; + } +} + +/** + * Given an already fetched service, ensures that the latest revision + * has a tagged traffic target. + * If the service does not have a tagged target already, the service will be modified + * to include a new target and the change will be publisehd to prod. + * Returns a map of region to map of service to latest tag. + */ +export async function ensureLatestRevisionTagged( + services: run.Service[], + defaultTag: string, +): Promise>> { + // Region -> Service -> Tag + const tags: Record> = {}; + const updateServices: Array> = []; + for (const service of services) { + const { projectNumber, region, serviceId } = run.gcpIds(service); + tags[region] = tags[region] || {}; + const latestRevision = service.status?.latestReadyRevisionName; + if (!latestRevision) { + throw new FirebaseError( + `Assertion failed: service ${service.metadata.name} has no ready revision`, + ); + } + const alreadyTagged = service.spec.traffic.find( + (target) => target.revisionName === latestRevision && target.tag, + ); + if (alreadyTagged) { + // Null assertion is safe because the predicate that found alreadyTagged + // checked for tag. + tags[region][serviceId] = alreadyTagged.tag!; + continue; + } + tags[region][serviceId] = defaultTag; + service.spec.traffic.push({ + revisionName: latestRevision, + tag: defaultTag, + }); + updateServices.push( + run.updateService( + `projects/${projectNumber}/locations/${region}/services/${serviceId}`, + service, + ), + ); + } + + await Promise.all(updateServices); + return tags; +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 464f789b236..00000000000 --- a/src/index.js +++ /dev/null @@ -1,99 +0,0 @@ -"use strict"; - -var program = require("commander"); -var pkg = require("../package.json"); -var clc = require("cli-color"); -const { logger } = require("./logger"); -var { setupLoggers } = require("./utils"); -var leven = require("leven"); - -program.version(pkg.version); -program.option( - "-P, --project ", - "the Firebase project to use for this command" -); -program.option("--account ", "the Google account to use for authorization"); -program.option("-j, --json", "output JSON instead of text, also triggers non-interactive mode"); -program.option("--token ", "supply an auth token for this command"); -program.option("--non-interactive", "error out of the command instead of waiting for prompts"); -program.option("-i, --interactive", "force prompts to be displayed"); -program.option("--debug", "print verbose debug output and keep a debug log file"); -program.option("-c, --config ", "path to the firebase.json file to use for configuration"); - -var client = {}; -client.cli = program; -client.logger = require("./logger"); -client.errorOut = require("./errorOut").errorOut; -client.getCommand = function (name) { - for (var i = 0; i < client.cli.commands.length; i++) { - if (client.cli.commands[i]._name === name) { - return client.cli.commands[i]; - } - } - return null; -}; - -require("./commands")(client); - -/** - * Checks to see if there is a different command similar to the provided one. - * This prints the suggestion and returns it if there is one. - * @param {string} cmd The command as provided by the user. - * @param {string[]} cmdList List of commands available in the CLI. - * @return {string|undefined} Returns the suggested command; undefined if none. - */ -function suggestCommands(cmd, cmdList) { - var suggestion = cmdList.find(function (c) { - return leven(c, cmd) < c.length * 0.4; - }); - if (suggestion) { - logger.error(); - logger.error("Did you mean " + clc.bold(suggestion) + "?"); - return suggestion; - } -} - -var commandNames = program.commands.map(function (cmd) { - return cmd._name; -}); - -var RENAMED_COMMANDS = { - "delete-site": "hosting:disable", - "disable:hosting": "hosting:disable", - "data:get": "database:get", - "data:push": "database:push", - "data:remove": "database:remove", - "data:set": "database:set", - "data:update": "database:update", - "deploy:hosting": "deploy --only hosting", - "deploy:database": "deploy --only database", - "prefs:token": "login:ci", -}; - -// Default handler, this is called when no other command action matches. -program.action(function (_, args) { - setupLoggers(); - - var cmd = args[0]; - logger.error(clc.bold.red("Error:"), clc.bold(cmd), "is not a Firebase command"); - - if (RENAMED_COMMANDS[cmd]) { - logger.error(); - logger.error( - clc.bold(cmd) + " has been renamed, please run", - clc.bold("firebase " + RENAMED_COMMANDS[cmd]), - "instead" - ); - } else { - // Check if the first argument is close to a command. - if (!suggestCommands(cmd, commandNames)) { - // Check to see if combining the two arguments comes close to a command. - // e.g. `firebase hosting disable` may suggest `hosting:disable`. - suggestCommands(args.join(":"), commandNames); - } - } - - process.exit(1); -}); - -module.exports = client; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000000..19cc38907d7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,104 @@ +import * as program from "commander"; +import * as clc from "colorette"; +import * as leven from "leven"; + +import { logger } from "./logger"; +import { setupLoggers } from "./utils"; + +const pkg = require("../package.json"); + +program.version(pkg.version); +program.option( + "-P, --project ", + "the Firebase project to use for this command", +); +program.option("--account ", "the Google account to use for authorization"); +program.option("-j, --json", "output JSON instead of text, also triggers non-interactive mode"); +program.option( + "--token ", + "DEPRECATED - will be removed in a future major version - supply an auth token for this command", +); +program.option("--non-interactive", "error out of the command instead of waiting for prompts"); +program.option("-i, --interactive", "force prompts to be displayed"); +program.option("--debug", "print verbose debug output and keep a debug log file"); +program.option("-c, --config ", "path to the firebase.json file to use for configuration"); + +const client = { + cli: program, + logger: require("./logger"), + errorOut: require("./errorOut").errorOut, + getCommand: (name: string) => { + for (let i = 0; i < client.cli.commands.length; i++) { + if (client.cli.commands[i]._name === name) { + return client.cli.commands[i]; + } + } + return; + }, +}; + +require("./commands").load(client); + +/** + * Checks to see if there is a different command similar to the provided one. + * This prints the suggestion and returns it if there is one. + * @param cmd The command as provided by the user. + * @param cmdList List of commands available in the CLI. + * @return Returns the suggested command; undefined if none. + */ +function suggestCommands(cmd: string, cmdList: string[]): string | undefined { + const suggestion = cmdList.find((c) => { + return leven(c, cmd) < c.length * 0.4; + }); + if (suggestion) { + logger.error(); + logger.error("Did you mean " + clc.bold(suggestion) + "?"); + return suggestion; + } +} + +const commandNames = program.commands.map((cmd: any) => { + return cmd._name; +}); + +const RENAMED_COMMANDS: Record = { + "delete-site": "hosting:disable", + "disable:hosting": "hosting:disable", + "data:get": "database:get", + "data:push": "database:push", + "data:remove": "database:remove", + "data:set": "database:set", + "data:update": "database:update", + "deploy:hosting": "deploy --only hosting", + "deploy:database": "deploy --only database", + "prefs:token": "login:ci", +}; + +// Default handler, this is called when no other command action matches. +program.action((_, args) => { + setupLoggers(); + + const cmd = args[0]; + logger.error(clc.bold(clc.red("Error:")), clc.bold(cmd), "is not a Firebase command"); + + if (RENAMED_COMMANDS[cmd]) { + logger.error(); + logger.error( + clc.bold(cmd) + " has been renamed, please run", + clc.bold("firebase " + RENAMED_COMMANDS[cmd]), + "instead", + ); + } else { + // Check if the first argument is close to a command. + if (!suggestCommands(cmd, commandNames)) { + // Check to see if combining the two arguments comes close to a command. + // e.g. `firebase hosting disable` may suggest `hosting:disable`. + suggestCommands(args.join(":"), commandNames); + } + } + + process.exit(1); +}); + +// NB: Keep this export line to keep firebase-tools-as-a-module working. +export = client; diff --git a/src/init/features/account.ts b/src/init/features/account.ts index be999547d0d..a40184bb7d3 100644 --- a/src/init/features/account.ts +++ b/src/init/features/account.ts @@ -5,16 +5,16 @@ import { loginAdditionalAccount, setActiveAccount, findAccountByEmail, - Account, setProjectAccount, } from "../../auth"; +import { Account } from "../../types/auth"; import { promptOnce } from "../../prompt"; import { FirebaseError } from "../../error"; async function promptForAccount() { logger.info(); logger.info( - `Which account do you want to use for this project? Choose an account or add a new one now` + `Which account do you want to use for this project? Choose an account or add a new one now`, ); logger.info(); @@ -38,7 +38,7 @@ async function promptForAccount() { choices, }); - if (emailChoice == "__add__") { + if (emailChoice === "__add__") { const newAccount = await loginAdditionalAccount(/* useLocalhost= */ true); if (!newAccount) { throw new FirebaseError("Failed to add new account", { exit: 1 }); diff --git a/src/init/features/database.ts b/src/init/features/database.ts index 1d68259cb11..fcaefb3eb77 100644 --- a/src/init/features/database.ts +++ b/src/init/features/database.ts @@ -1,5 +1,4 @@ -import * as clc from "cli-color"; -import * as api from "../../api"; +import * as clc from "colorette"; import { prompt, promptOnce } from "../../prompt"; import { logger } from "../../logger"; import * as utils from "../../utils"; @@ -13,10 +12,12 @@ import { checkInstanceNameAvailable, getDatabaseInstanceDetails, } from "../../management/database"; -import ora = require("ora"); +import * as ora from "ora"; import { ensure } from "../../ensureApiEnabled"; import { getDefaultDatabaseInstance } from "../../getDefaultDatabaseInstance"; import { FirebaseError } from "../../error"; +import { Client } from "../../apiv2"; +import { rtdbManagementOrigin } from "../../api"; interface DatabaseSetup { projectId?: string; @@ -34,32 +35,38 @@ interface DatabaseSetupConfig { const DEFAULT_RULES = JSON.stringify( { rules: { ".read": "auth != null", ".write": "auth != null" } }, null, - 2 + 2, ); async function getDBRules(instanceDetails: DatabaseInstance): Promise { if (!instanceDetails || !instanceDetails.name) { return DEFAULT_RULES; } - const response = await api.request("GET", "/.settings/rules.json", { - auth: true, - origin: instanceDetails.databaseUrl, + const client = new Client({ urlPrefix: instanceDetails.databaseUrl }); + const response = await client.request({ + method: "GET", + path: "/.settings/rules.json", + responseType: "stream", + resolveOnHTTPError: true, }); - return response.body; + if (response.status !== 200) { + throw new FirebaseError(`Failed to fetch current rules. Code: ${response.status}`); + } + return await response.response.text(); } function writeDBRules( rules: string, logMessagePrefix: string, filename: string, - config: Config + config: Config, ): void { config.writeProjectFile(filename, rules); utils.logSuccess(`${logMessagePrefix} have been written to ${clc.bold(filename)}.`); logger.info( `Future modifications to ${clc.bold( - filename - )} will update Realtime Database Security Rules when you run` + filename, + )} will update Realtime Database Security Rules when you run`, ); logger.info(clc.bold("firebase deploy") + "."); } @@ -80,21 +87,21 @@ async function createDefaultDatabaseInstance(project: string): Promise { + let info: RequiredInfo = { + serviceId: "", + locationId: "", + cloudSqlInstanceId: "", + isNewInstance: false, + cloudSqlDatabase: "", + isNewDatabase: false, + connectorId: "default-connector", + }; + info = await promptForService(setup, info); + + if (info.cloudSqlInstanceId === "") { + info = await promptForCloudSQLInstance(setup, info); + } + + if (info.cloudSqlDatabase === "") { + info = await promptForDatabase(setup, config, info); + } + + // TODO: Remove this in favor of a better way of setting local connection string. + const defaultConnectionString = + setup.rcfile.dataconnectEmulatorConfig?.postgres?.localConnectionString ?? + DEFAULT_POSTGRES_CONNECTION; + // TODO: Download Postgres + const localConnectionString = await promptOnce({ + type: "input", + name: "localConnectionString", + message: `What is the connection string of the local Postgres instance you would like to use with the Data Connect emulator?`, + default: defaultConnectionString, + }); + setup.rcfile.dataconnectEmulatorConfig = { postgres: { localConnectionString } }; + + const dir: string = config.get("dataconnect.source") || "dataconnect"; + const subbedDataconnectYaml = subValues(DATACONNECT_YAML_TEMPLATE, info); + const subbedConnectorYaml = subValues(CONNECTOR_YAML_TEMPLATE, info); + + config.set("dataconnect", { source: dir }); + await config.askWriteProjectFile(join(dir, "dataconnect.yaml"), subbedDataconnectYaml); + await config.askWriteProjectFile(join(dir, "schema", "schema.gql"), SCHEMA_TEMPLATE); + await config.askWriteProjectFile( + join(dir, info.connectorId, "connector.yaml"), + subbedConnectorYaml, + ); + await config.askWriteProjectFile(join(dir, info.connectorId, "queries.gql"), QUERIES_TEMPLATE); + await config.askWriteProjectFile( + join(dir, info.connectorId, "mutations.gql"), + MUTATIONS_TEMPLATE, + ); + + if ( + setup.projectId && + (info.isNewInstance || info.isNewDatabase) && + (await confirm({ + message: + "Would you like to provision your CloudSQL instance and database now? This will take a few minutes.", + default: true, + })) + ) { + await provisionCloudSql({ + projectId: setup.projectId, + locationId: info.locationId, + instanceId: info.cloudSqlInstanceId, + databaseId: info.cloudSqlDatabase, + enableGoogleMlIntegration: false, + waitForCreation: false, + }); + } + logger.info(""); + logSuccess( + `If you'd like to generate an SDK for your new connector, run ${clc.bold("firebase init dataconnect:sdk")}`, + ); +} + +function subValues( + template: string, + replacementValues: { + serviceId: string; + cloudSqlInstanceId: string; + cloudSqlDatabase: string; + connectorId: string; + locationId: string; + }, +): string { + const replacements: Record = { + serviceId: "__serviceId__", + cloudSqlDatabase: "__cloudSqlDatabase__", + cloudSqlInstanceId: "__cloudSqlInstanceId__", + connectorId: "__connectorId__", + locationId: "__location__", + }; + let replaced = template; + for (const [k, v] of Object.entries(replacementValues)) { + replaced = replaced.replace(replacements[k], v); + } + return replaced; +} + +async function promptForService(setup: Setup, info: RequiredInfo): Promise { + if (setup.projectId) { + await ensureApis(setup.projectId); + // TODO (b/344021748): Support initing with services that have existing sources/files + const existingServices = await listAllServices(setup.projectId); + const existingServicesAndSchemas = await Promise.all( + existingServices.map(async (s) => { + return { + service: s, + schema: await getSchema(s.name), + }; + }), + ); + const existingFreshServicesAndSchemas = existingServicesAndSchemas.filter((s) => { + return !s.schema?.source.files?.length; + }); + if (existingFreshServicesAndSchemas.length) { + const choices: { name: string; value: any }[] = existingFreshServicesAndSchemas.map((s) => { + const serviceName = parseServiceName(s.service.name); + return { + name: `${serviceName.location}/${serviceName.serviceId}`, + value: s, + }; + }); + choices.push({ name: "Create a new service", value: undefined }); + const choice: { service: Service; schema: Schema } = await promptOnce({ + message: + "Your project already has existing services. Which would you like to set up local files for?", + type: "list", + choices, + }); + if (choice) { + const serviceName = parseServiceName(choice.service.name); + info.serviceId = serviceName.serviceId; + info.locationId = serviceName.location; + if (choice.schema) { + if (choice.schema.primaryDatasource.postgresql?.cloudSql.instance) { + const instanceName = parseCloudSQLInstanceName( + choice.schema.primaryDatasource.postgresql?.cloudSql.instance, + ); + info.cloudSqlInstanceId = instanceName.instanceId; + } + info.cloudSqlDatabase = choice.schema.primaryDatasource.postgresql?.database ?? ""; + } + } + } + } + + if (info.serviceId === "") { + info.serviceId = await promptOnce({ + message: "What ID would you like to use for this service?", + type: "input", + default: "my-service", + }); + } + return info; +} + +async function promptForCloudSQLInstance(setup: Setup, info: RequiredInfo): Promise { + if (setup.projectId) { + const instances = await cloudsql.listInstances(setup.projectId); + let choices = instances.map((i) => { + return { name: i.name, value: i.name, location: i.region }; + }); + // If we've already chosen a region (ie service already exists), only list instances from that region. + choices = choices.filter((c) => info.locationId === "" || info.locationId === c.location); + if (choices.length) { + const freeTrialInstanceId = await checkForFreeTrialInstance(setup.projectId); + if (!freeTrialInstanceId) { + choices.push({ name: "Create a new instance", value: "", location: "" }); + } + info.cloudSqlInstanceId = await promptOnce({ + message: `Which CloudSQL instance would you like to use?`, + type: "list", + choices, + }); + if (info.cloudSqlInstanceId !== "") { + // Infer location if a CloudSQL instance is chosen. + info.locationId = choices.find((c) => c.value === info.cloudSqlInstanceId)!.location; + } + } + } + if (info.cloudSqlInstanceId === "") { + info.isNewInstance = true; + info.cloudSqlInstanceId = await promptOnce({ + message: `What ID would you like to use for your new CloudSQL instance?`, + type: "input", + default: `fdc-sql`, + }); + } + if (info.locationId === "") { + const choices = await locationChoices(setup); + info.locationId = await promptOnce({ + message: "What location would like to use?", + type: "list", + choices, + }); + } + return info; +} + +async function locationChoices(setup: Setup) { + if (setup.projectId) { + const locations = await listLocations(setup.projectId); + return locations.map((l) => { + return { name: l, value: l }; + }); + } else { + // Hardcoded locations for when there is no project set up. + return [ + { name: "us-central1", value: "us-central1" }, + { name: "europe-north1", value: "europe-north1" }, + { name: "europe-central2", value: "europe-central2" }, + { name: "europe-west1", value: "europe-west1" }, + { name: "southamerica-west1", value: "southamerica-west1" }, + { name: "us-east4", value: "us-east4" }, + { name: "us-west1", value: "us-west1" }, + { name: "asia-southeast1", value: "asia-southeast1" }, + ]; + } +} + +async function promptForDatabase( + setup: Setup, + config: Config, + info: RequiredInfo, +): Promise { + if (!info.isNewInstance && setup.projectId) { + try { + const dbs = await cloudsql.listDatabases(setup.projectId, info.cloudSqlInstanceId); + const choices = dbs.map((d) => { + return { name: d.name, value: d.name }; + }); + choices.push({ name: "Create a new database", value: "" }); + if (dbs.length) { + info.cloudSqlDatabase = await promptOnce({ + message: `Which database in ${info.cloudSqlInstanceId} would you like to use?`, + type: "list", + choices, + }); + } + } catch (err) { + // Show existing databases in a list is optional, ignore any errors from ListDatabases. + // This often happen when the Cloud SQL instance is still being created. + logger.debug(`[dataconnect] Cannot list databases during init: ${err}`); + } + } + if (info.cloudSqlDatabase === "") { + info.isNewDatabase = true; + info.cloudSqlDatabase = await promptOnce({ + message: `What ID would you like to use for your new database in ${info.cloudSqlInstanceId}?`, + type: "input", + default: `fdcdb`, + }); + } + return info; +} diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts new file mode 100644 index 00000000000..a2dd999639c --- /dev/null +++ b/src/init/features/dataconnect/sdk.ts @@ -0,0 +1,137 @@ +import * as yaml from "yaml"; +import * as fs from "fs-extra"; +import { confirm, promptOnce } from "../../../prompt"; +import * as clc from "colorette"; +import * as path from "path"; +import { readFirebaseJson } from "../../../dataconnect/fileUtils"; +import { Config } from "../../../config"; +import { Setup } from "../.."; +import { load } from "../../../dataconnect/load"; +import { logger } from "../../../logger"; +import { ConnectorInfo, ConnectorYaml, JavascriptSDK, KotlinSDK } from "../../../dataconnect/types"; +import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; + +const IOS = "ios"; +const WEB = "web"; +const ANDROID = "android"; +export async function doSetup(setup: Setup, config: Config): Promise { + const serviceCfgs = readFirebaseJson(config); + const serviceInfos = await Promise.all( + serviceCfgs.map((c) => load(setup.projectId || "", path.join(process.cwd(), c.source))), + ); + const connectorChoices: { name: string; value: ConnectorInfo }[] = serviceInfos + .map((si) => { + return si.connectorInfo.map((ci) => { + return { + name: `${si.dataConnectYaml.serviceId}/${ci.connectorYaml.connectorId}`, + value: ci, + }; + }); + }) + .flat(); + if (!connectorChoices.length) { + logger.info( + `Your config has no connectors to set up SDKs for. Run ${clc.bold( + "firebase init dataconnect", + )} to set up a service and conenctors.`, + ); + return; + } + const connectorInfo: ConnectorInfo = await promptOnce({ + message: "Which connector do you want set up a generated SDK for?", + type: "list", + choices: connectorChoices, + }); + + const platforms = await promptOnce({ + message: "Which platforms do you want to set up a generated SDK for?", + type: "checkbox", + choices: [ + { name: "iOS (Swift)", value: IOS }, + { name: "Web (JavaScript)", value: WEB }, + { name: "Androd (Kotlin)", value: ANDROID }, + ], + }); + + const newConnectorYaml = JSON.parse(JSON.stringify(connectorInfo.connectorYaml)) as ConnectorYaml; + if (!newConnectorYaml.generate) { + newConnectorYaml.generate = {}; + } + + if (platforms.includes(IOS)) { + const defaultOutputDir = newConnectorYaml.generate.swiftSdk?.outputDir; + const outputDir = await promptOnce({ + message: `What directory do you want to write your Swift SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, + type: "input", + default: defaultOutputDir, + }); + const swiftSdk = { outputDir }; + newConnectorYaml.generate.swiftSdk = swiftSdk; + } + if (platforms.includes(WEB)) { + const outputDir = await promptOnce({ + message: `What directory do you want to write your JavaScript SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, + type: "input", + default: newConnectorYaml.generate.javascriptSdk?.outputDir, + }); + const pkg = await promptOnce({ + message: "What package name do you want to use for your JavaScript SDK?", + type: "input", + default: + newConnectorYaml.generate.javascriptSdk?.package ?? + `@firebasegen/${connectorInfo.connectorYaml.connectorId}`, + }); + const packageJSONDir = await promptOnce({ + message: + "Which directory contains the package.json that you would like to add the JavaScript SDK dependency to? (Leave blank to skip)", + type: "input", + default: newConnectorYaml.generate.javascriptSdk?.packageJSONDir, + }); + // ../.. since we ask relative to connector.yaml + const javascriptSdk: JavascriptSDK = { + outputDir, + package: pkg, + }; + if (packageJSONDir) { + javascriptSdk.packageJSONDir = packageJSONDir; + } + newConnectorYaml.generate.javascriptSdk = javascriptSdk; + } + if (platforms.includes(ANDROID)) { + const outputDir = await promptOnce({ + message: `What directory do you want to write your Kotlin SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, + type: "input", + default: newConnectorYaml.generate.kotlinSdk?.outputDir, + }); + const pkg = await promptOnce({ + message: "What package name do you want to use for your Kotlin SDK?", + type: "input", + default: + newConnectorYaml.generate.kotlinSdk?.package ?? + `com.google.firebase.dataconnect.connectors.${connectorInfo.connectorYaml.connectorId}`, + }); + const kotlinSdk: KotlinSDK = { + outputDir, + package: pkg, + }; + newConnectorYaml.generate.kotlinSdk = kotlinSdk; + } + // TODO: Prompt user about adding generated paths to .gitignore + const connectorYamlContents = yaml.stringify(newConnectorYaml); + const connectorYamlPath = `${connectorInfo.directory}/connector.yaml`; + fs.writeFileSync(connectorYamlPath, connectorYamlContents, "utf8"); + logger.info(`Wrote new config to ${connectorYamlPath}`); + if ( + setup.projectId && + (await confirm({ + message: "Would you like to generate SDK code now?", + default: true, + })) + ) { + await DataConnectEmulator.generate({ + configDir: connectorInfo.directory, + connectorId: connectorInfo.connectorYaml.connectorId, + }); + logger.info(`Generated SDK code for ${connectorInfo.connectorYaml.connectorId}`); + } +} diff --git a/src/init/features/emulators.ts b/src/init/features/emulators.ts index c3f61a0c3c8..87ea621b629 100644 --- a/src/init/features/emulators.ts +++ b/src/init/features/emulators.ts @@ -1,17 +1,21 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as _ from "lodash"; import * as utils from "../../utils"; -import { prompt } from "../../prompt"; +import { prompt, promptOnce } from "../../prompt"; import { Emulators, ALL_SERVICE_EMULATORS, isDownloadableEmulator } from "../../emulator/types"; import { Constants } from "../../emulator/constants"; import { downloadIfNecessary } from "../../emulator/downloadableEmulators"; +import { Setup } from "../index"; interface EmulatorsInitSelections { emulators?: Emulators[]; download?: boolean; } -export async function doSetup(setup: any, config: any) { +// postgresql://localhost:5432 is a default out of the box value for most installations of Postgres +export const DEFAULT_POSTGRES_CONNECTION = "postgresql://localhost:5432?sslmode=disable"; + +export async function doSetup(setup: Setup, config: any) { const choices = ALL_SERVICE_EMULATORS.map((e) => { return { value: e, @@ -79,7 +83,7 @@ export async function doSetup(setup: any, config: any) { type: "input", name: "port", message: `Which port do you want to use for the ${clc.underline( - uiDesc + uiDesc, )} (leave empty to use any available port)?`, }, ]); @@ -90,16 +94,35 @@ export async function doSetup(setup: any, config: any) { } } + if (selections.emulators.includes(Emulators.DATACONNECT)) { + const defaultConnectionString = + setup.rcfile.dataconnectEmulatorConfig?.postgres?.localConnectionString ?? + DEFAULT_POSTGRES_CONNECTION; + // TODO: Download Postgres + const localConnectionString = await promptOnce({ + type: "input", + name: "localConnectionString", + message: `What is the connection string of the local Postgres instance you would like to use with the Data Connect emulator?`, + default: defaultConnectionString, + }); + setup.rcfile.dataconnectEmulatorConfig = { postgres: { localConnectionString } }; + } + await prompt(selections, [ { name: "download", type: "confirm", message: "Would you like to download the emulators now?", - default: false, + default: true, }, ]); } + // Set the default behavior to be single project mode. + if (setup.config.emulators.singleProjectMode === undefined) { + setup.config.emulators.singleProjectMode = true; + } + if (selections.download) { for (const selected of selections.emulators) { if (isDownloadableEmulator(selected)) { diff --git a/src/init/features/extensions/index.ts b/src/init/features/extensions/index.ts new file mode 100644 index 00000000000..1ab70427878 --- /dev/null +++ b/src/init/features/extensions/index.ts @@ -0,0 +1,18 @@ +import { requirePermissions } from "../../../requirePermissions"; +import { Options } from "../../../options"; +import { ensure } from "../../../ensureApiEnabled"; +import { Config } from "../../../config"; +import * as manifest from "../../../extensions/manifest"; +import { extensionsOrigin } from "../../../api"; + +/** + * Set up a new firebase project for extensions. + */ +export async function doSetup(setup: any, config: Config, options: Options): Promise { + const projectId = setup?.rcfile?.projects?.default; + if (projectId) { + await requirePermissions({ ...options, project: projectId }); + await Promise.all([ensure(projectId, extensionsOrigin(), "unused", true)]); + } + return manifest.writeEmptyManifest(config, options); +} diff --git a/src/init/features/firestore.spec.ts b/src/init/features/firestore.spec.ts new file mode 100644 index 00000000000..28b8de116f9 --- /dev/null +++ b/src/init/features/firestore.spec.ts @@ -0,0 +1,72 @@ +import { expect } from "chai"; +import * as _ from "lodash"; +import * as sinon from "sinon"; + +import { FirebaseError } from "../../error"; +import * as firestore from "./firestore"; +import * as indexes from "./firestore/indexes"; +import * as rules from "./firestore/rules"; +import * as requirePermissions from "../../requirePermissions"; +import * as apiEnabled from "../../ensureApiEnabled"; +import * as checkDatabaseType from "../../firestore/checkDatabaseType"; + +describe("firestore", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let checkApiStub: sinon.SinonStub; + let checkDbTypeStub: sinon.SinonStub; + + beforeEach(() => { + checkApiStub = sandbox.stub(apiEnabled, "check"); + checkDbTypeStub = sandbox.stub(checkDatabaseType, "checkDatabaseType"); + + // By default, mock Firestore enabled in Native mode + checkApiStub.returns(true); + checkDbTypeStub.returns("FIRESTORE_NATIVE"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("doSetup", () => { + it("should require access, set up rules and indices, ensure cloud resource location set", async () => { + const requirePermissionsStub = sandbox + .stub(requirePermissions, "requirePermissions") + .resolves(); + const initIndexesStub = sandbox.stub(indexes, "initIndexes").resolves(); + const initRulesStub = sandbox.stub(rules, "initRules").resolves(); + + const setup = { config: {}, projectId: "my-project-123", projectLocation: "us-central1" }; + + await firestore.doSetup(setup, {}, {}); + + expect(requirePermissionsStub).to.have.been.calledOnce; + expect(initRulesStub).to.have.been.calledOnce; + expect(initIndexesStub).to.have.been.calledOnce; + expect(_.get(setup, "config.firestore")).to.deep.equal({}); + }); + + it("should error when the firestore API is not enabled", async () => { + checkApiStub.returns(false); + + const setup = { config: {}, projectId: "my-project-123" }; + + await expect(firestore.doSetup(setup, {}, {})).to.eventually.be.rejectedWith( + FirebaseError, + "It looks like you haven't used Cloud Firestore", + ); + }); + + it("should error when firestore is in the wrong mode", async () => { + checkApiStub.returns(true); + checkDbTypeStub.returns("CLOUD_DATASTORE_COMPATIBILITY"); + + const setup = { config: {}, projectId: "my-project-123" }; + + await expect(firestore.doSetup(setup, {}, {})).to.eventually.be.rejectedWith( + FirebaseError, + "It looks like this project is using Cloud Datastore or Cloud Firestore in Datastore mode.", + ); + }); + }); +}); diff --git a/src/init/features/firestore/index.ts b/src/init/features/firestore/index.ts index 45fb4db878a..b40f01cac77 100644 --- a/src/init/features/firestore/index.ts +++ b/src/init/features/firestore/index.ts @@ -1,20 +1,19 @@ import { logger } from "../../../logger"; import * as apiEnabled from "../../../ensureApiEnabled"; -import { ensureLocationSet } from "../../../ensureCloudResourceLocation"; import { requirePermissions } from "../../../requirePermissions"; import { checkDatabaseType } from "../../../firestore/checkDatabaseType"; import * as rules from "./rules"; import * as indexes from "./indexes"; import { FirebaseError } from "../../../error"; -import * as clc from "cli-color"; +import * as clc from "colorette"; async function checkProjectSetup(setup: any, config: any, options: any) { const firestoreUnusedError = new FirebaseError( - `It looks like you haven't used Cloud Firestore in this project before. Go to ${clc.bold.underline( - `https://console.firebase.google.com/project/${setup.projectId}/firestore` + `It looks like you haven't used Cloud Firestore in this project before. Go to ${clc.bold( + clc.underline(`https://console.firebase.google.com/project/${setup.projectId}/firestore`), )} to create your Cloud Firestore database.`, - { exit: 1 } + { exit: 1 }, ); // First check if the Firestore API is enabled. If it's not, then the developer needs @@ -23,7 +22,7 @@ async function checkProjectSetup(setup: any, config: any, options: any) { setup.projectId, "firestore.googleapis.com", "", - true + true, ); if (!isFirestoreEnabled) { throw firestoreUnusedError; @@ -36,14 +35,13 @@ async function checkProjectSetup(setup: any, config: any, options: any) { if (!dbType) { throw firestoreUnusedError; - } else if (dbType !== "CLOUD_FIRESTORE") { + } else if (dbType !== "FIRESTORE_NATIVE") { throw new FirebaseError( `It looks like this project is using Cloud Datastore or Cloud Firestore in Datastore mode. The Firebase CLI can only manage projects using Cloud Firestore in Native mode. For more information, visit https://cloud.google.com/datastore/docs/firestore-or-datastore`, - { exit: 1 } + { exit: 1 }, ); } - ensureLocationSet(setup.projectLocation, "Cloud Firestore"); await requirePermissions({ ...options, project: setup.projectId }); } diff --git a/src/init/features/firestore/indexes.ts b/src/init/features/firestore/indexes.ts index 029a946d9cb..6fe2876eb6c 100644 --- a/src/init/features/firestore/indexes.ts +++ b/src/init/features/firestore/indexes.ts @@ -1,18 +1,15 @@ -import clc = require("cli-color"); -import fs = require("fs"); +import * as clc from "colorette"; import { FirebaseError } from "../../../error"; -import iv2 = require("../../../firestore/indexes"); -import fsutils = require("../../../fsutils"); +import * as api from "../../../firestore/api"; +import * as fsutils from "../../../fsutils"; import { prompt, promptOnce } from "../../../prompt"; import { logger } from "../../../logger"; +import { readTemplateSync } from "../../../templates"; -const indexes = new iv2.FirestoreIndexes(); +const indexes = new api.FirestoreApi(); -const INDEXES_TEMPLATE = fs.readFileSync( - __dirname + "/../../../../templates/init/firestore/firestore.indexes.json", - "utf8" -); +const INDEXES_TEMPLATE = readTemplateSync("init/firestore/firestore.indexes.json"); export function initIndexes(setup: any, config: any): Promise { logger.info(); diff --git a/src/init/features/firestore/rules.ts b/src/init/features/firestore/rules.ts index 2824025d6e8..136365bb6d3 100644 --- a/src/init/features/firestore/rules.ts +++ b/src/init/features/firestore/rules.ts @@ -1,18 +1,15 @@ -import clc = require("cli-color"); -import fs = require("fs"); +import * as clc from "colorette"; -import gcp = require("../../../gcp"); -import fsutils = require("../../../fsutils"); +import * as gcp from "../../../gcp"; +import * as fsutils from "../../../fsutils"; import { prompt, promptOnce } from "../../../prompt"; import { logger } from "../../../logger"; -import utils = require("../../../utils"); +import * as utils from "../../../utils"; +import { readTemplateSync } from "../../../templates"; const DEFAULT_RULES_FILE = "firestore.rules"; -const RULES_TEMPLATE = fs.readFileSync( - __dirname + "/../../../../templates/init/firestore/firestore.rules", - "utf8" -); +const RULES_TEMPLATE = readTemplateSync("init/firestore/firestore.rules"); export function initRules(setup: any, config: any): Promise { logger.info(); diff --git a/src/init/features/functions.spec.ts b/src/init/features/functions.spec.ts new file mode 100644 index 00000000000..532b480535b --- /dev/null +++ b/src/init/features/functions.spec.ts @@ -0,0 +1,219 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as prompt from "../../prompt"; +import { Config } from "../../config"; +import { Setup } from ".."; +import { doSetup } from "./functions"; +import { Options } from "../../options"; +import { RC } from "../../rc"; + +const TEST_SOURCE_DEFAULT = "functions"; +const TEST_CODEBASE_DEFAULT = "default"; + +function createExistingTestSetupAndConfig(): { setup: Setup; config: Config } { + const cbconfig = { + source: TEST_SOURCE_DEFAULT, + codebase: TEST_CODEBASE_DEFAULT, + ignore: ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"], + predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'], + }; + + return { + setup: { + config: { + functions: [cbconfig], + }, + rcfile: { projects: {}, targets: {}, etags: {}, dataconnectEmulatorConfig: {} }, + featureArg: true, + }, + config: new Config({ functions: [cbconfig] }, { projectDir: "test", cwd: "test" }), + }; +} + +describe("functions", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let promptOnceStub: sinon.SinonStub; + let promptStub: sinon.SinonStub; + let askWriteProjectFileStub: sinon.SinonStub; + let emptyConfig: Config; + let options: Options; + + beforeEach(() => { + promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call"); + promptStub = sandbox.stub(prompt, "prompt").throws("Unexpected prompt call"); + + emptyConfig = new Config("{}", {}); + options = { + cwd: "", + configPath: "", + only: "", + except: "", + filteredTargets: [], + force: false, + json: false, + nonInteractive: false, + interactive: false, + debug: false, + config: emptyConfig, + rc: new RC(), + }; + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + describe("doSetup", () => { + describe("with an uninitialized Firebase project repository", () => { + it("creates a new javascript codebase with the correct configuration", async () => { + const setup = { config: { functions: [] }, rcfile: {} }; + promptOnceStub.onFirstCall().resolves("javascript"); + + // say "yes" to enabling eslint for the js project + promptStub.onFirstCall().callsFake((functions: any): Promise => { + functions.lint = true; + return Promise.resolve(); + }); + // do not install dependencies + promptStub.onSecondCall().resolves(); + askWriteProjectFileStub = sandbox.stub(emptyConfig, "askWriteProjectFile"); + askWriteProjectFileStub.resolves(); + + await doSetup(setup, emptyConfig, options); + + expect(setup.config.functions[0]).to.deep.equal({ + source: TEST_SOURCE_DEFAULT, + codebase: TEST_CODEBASE_DEFAULT, + ignore: ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"], + predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'], + }); + expect(askWriteProjectFileStub.getCalls().map((call) => call.args[0])).to.deep.equal([ + `${TEST_SOURCE_DEFAULT}/package.json`, + `${TEST_SOURCE_DEFAULT}/.eslintrc.js`, + `${TEST_SOURCE_DEFAULT}/index.js`, + `${TEST_SOURCE_DEFAULT}/.gitignore`, + ]); + }); + + it("creates a new typescript codebase with the correct configuration", async () => { + const setup = { config: { functions: [] }, rcfile: {} }; + promptOnceStub.onFirstCall().resolves("typescript"); + promptStub.onFirstCall().callsFake((functions: any): Promise => { + functions.lint = true; + return Promise.resolve(); + }); + promptStub.onSecondCall().resolves(); + askWriteProjectFileStub = sandbox.stub(emptyConfig, "askWriteProjectFile"); + askWriteProjectFileStub.resolves(); + + await doSetup(setup, emptyConfig, options); + + expect(setup.config.functions[0]).to.deep.equal({ + source: TEST_SOURCE_DEFAULT, + codebase: TEST_CODEBASE_DEFAULT, + ignore: ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"], + predeploy: [ + 'npm --prefix "$RESOURCE_DIR" run lint', + 'npm --prefix "$RESOURCE_DIR" run build', + ], + }); + expect(askWriteProjectFileStub.getCalls().map((call) => call.args[0])).to.deep.equal([ + `${TEST_SOURCE_DEFAULT}/package.json`, + `${TEST_SOURCE_DEFAULT}/.eslintrc.js`, + `${TEST_SOURCE_DEFAULT}/tsconfig.json`, + `${TEST_SOURCE_DEFAULT}/tsconfig.dev.json`, + `${TEST_SOURCE_DEFAULT}/src/index.ts`, + `${TEST_SOURCE_DEFAULT}/.gitignore`, + ]); + }); + }); + describe("with an existing functions codebase in Firebase repository", () => { + it("initializes a new codebase", async () => { + const { setup, config } = createExistingTestSetupAndConfig(); + promptOnceStub.onCall(0).resolves("new"); + promptOnceStub.onCall(1).resolves("testcodebase2"); + promptOnceStub.onCall(2).resolves("testsource2"); + promptOnceStub.onCall(3).resolves("javascript"); + promptStub.onFirstCall().callsFake((functions: any): Promise => { + functions.lint = true; + return Promise.resolve(); + }); + promptStub.onSecondCall().resolves(); + askWriteProjectFileStub = sandbox.stub(config, "askWriteProjectFile"); + askWriteProjectFileStub.resolves(); + + await doSetup(setup, config, options); + + expect(setup.config.functions).to.deep.equal([ + { + source: TEST_SOURCE_DEFAULT, + codebase: TEST_CODEBASE_DEFAULT, + ignore: [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local", + ], + predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'], + }, + { + source: "testsource2", + codebase: "testcodebase2", + ignore: [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local", + ], + predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'], + }, + ]); + expect(askWriteProjectFileStub.getCalls().map((call) => call.args[0])).to.deep.equal([ + `testsource2/package.json`, + `testsource2/.eslintrc.js`, + `testsource2/index.js`, + `testsource2/.gitignore`, + ]); + }); + + it("reinitializes an existing codebase", async () => { + const { setup, config } = createExistingTestSetupAndConfig(); + promptOnceStub.onFirstCall().resolves("reinit"); + promptOnceStub.onSecondCall().resolves("javascript"); + promptStub.onFirstCall().callsFake((functions: any): Promise => { + functions.lint = true; + return Promise.resolve(); + }); + promptStub.onSecondCall().resolves(false); + askWriteProjectFileStub = sandbox.stub(config, "askWriteProjectFile"); + askWriteProjectFileStub.resolves(); + + await doSetup(setup, config, options); + + expect(setup.config.functions).to.deep.equal([ + { + source: TEST_SOURCE_DEFAULT, + codebase: TEST_CODEBASE_DEFAULT, + ignore: [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local", + ], + predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'], + }, + ]); + expect(askWriteProjectFileStub.getCalls().map((call) => call.args[0])).to.deep.equal([ + `${TEST_SOURCE_DEFAULT}/package.json`, + `${TEST_SOURCE_DEFAULT}/.eslintrc.js`, + `${TEST_SOURCE_DEFAULT}/index.js`, + `${TEST_SOURCE_DEFAULT}/.gitignore`, + ]); + }); + }); + }); +}); diff --git a/src/init/features/functions/golang.ts b/src/init/features/functions/golang.ts deleted file mode 100644 index 4e631114964..00000000000 --- a/src/init/features/functions/golang.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { promisify } from "util"; -import * as fs from "fs"; -import * as path from "path"; -import * as spawn from "cross-spawn"; - -import { FirebaseError } from "../../../error"; -import { Config } from "../../../config"; -import { promptOnce } from "../../../prompt"; -import * as utils from "../../../utils"; -import * as go from "../../../deploy/functions/runtimes/golang"; -import { logger } from "../../../logger"; - -const clc = require("cli-color"); - -const RUNTIME_VERSION = "1.13"; - -const TEMPLATE_ROOT = path.resolve(__dirname, "../../../../templates/init/functions/golang"); -const MAIN_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "functions.go"), "utf8"); -const GITIGNORE_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "_gitignore"), "utf8"); - -async function init(setup: unknown, config: Config) { - await writeModFile(config); - - const modName = config.get("functions.go.module") as string; - const [pkg] = modName.split("/").slice(-1); - await config.askWriteProjectFile("functions/functions.go", MAIN_TEMPLATE.replace("PACKAGE", pkg)); - await config.askWriteProjectFile("functions/.gitignore", GITIGNORE_TEMPLATE); -} - -// writeModFile is meant to look like askWriteProjectFile but it generates the contents -// dynamically using the go tool -async function writeModFile(config: Config) { - const modPath = config.path("functions/go.mod"); - if (await promisify(fs.exists)(modPath)) { - const shoudlWriteModFile = await promptOnce({ - type: "confirm", - message: "File " + clc.underline("functions/go.mod") + " already exists. Overwrite?", - default: false, - }); - if (!shoudlWriteModFile) { - return; - } - - // Go will refuse to overwrite an existing mod file. - await promisify(fs.unlink)(modPath); - } - - // Nit(inlined) can we look at functions code and see if there's a domain mapping? - const modName = await promptOnce({ - type: "input", - message: "What would you like to name your module?", - default: "acme.com/functions", - }); - config.set("functions.go.module", modName); - - // Manually create a go mod file because (A) it's easier this way and (B) it seems to be the only - // way to set the min Go version to anything but what the user has installed. - config.writeProjectFile("functions/go.mod", `module ${modName} \n\ngo ${RUNTIME_VERSION}\n\n`); - utils.logSuccess("Wrote " + clc.bold("functions/go.mod")); - - for (const dep of [go.FUNCTIONS_SDK, go.ADMIN_SDK, go.FUNCTIONS_CODEGEN, go.FUNCTIONS_RUNTIME]) { - const result = spawn.sync("go", ["get", dep], { - cwd: config.path("functions"), - stdio: "inherit", - }); - if (result.error) { - logger.debug("Full output from go get command:", JSON.stringify(result, null, 2)); - throw new FirebaseError("Error installing dependencies", { children: [result.error] }); - } - } - utils.logSuccess("Installed dependencies"); -} - -module.exports = init; diff --git a/src/init/features/functions/index.ts b/src/init/features/functions/index.ts index 007a9345376..3ac15033606 100644 --- a/src/init/features/functions/index.ts +++ b/src/init/features/functions/index.ts @@ -1,31 +1,168 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { logger } from "../../../logger"; import { promptOnce } from "../../../prompt"; import { requirePermissions } from "../../../requirePermissions"; -import { previews } from "../../../previews"; import { Options } from "../../../options"; import { ensure } from "../../../ensureApiEnabled"; +import { Config } from "../../../config"; +import { + normalizeAndValidate, + configForCodebase, + validateCodebase, + assertUnique, +} from "../../../functions/projectConfig"; +import { FirebaseError } from "../../../error"; +import { functionsOrigin, runtimeconfigOrigin } from "../../../api"; -module.exports = async function (setup: any, config: any, options: Options) { - logger.info(); - logger.info( - "A " + clc.bold("functions") + " directory will be created in your project with sample code" - ); - logger.info( - "pre-configured. Functions can be deployed with " + clc.bold("firebase deploy") + "." - ); - logger.info(); +const MAX_ATTEMPTS = 5; - setup.functions = {}; +/** + * Set up a new firebase project for functions. + */ +export async function doSetup(setup: any, config: Config, options: Options): Promise { const projectId = setup?.rcfile?.projects?.default; if (projectId) { await requirePermissions({ ...options, project: projectId }); await Promise.all([ - ensure(projectId, "cloudfunctions.googleapis.com", "unused", true), - ensure(projectId, "runtimeconfig.googleapis.com", "unused", true), + ensure(projectId, functionsOrigin(), "unused", true), + ensure(projectId, runtimeconfigOrigin(), "unused", true), ]); } + setup.functions = {}; + // check if functions have been initialized yet + if (!config.src.functions) { + setup.config.functions = []; + return initNewCodebase(setup, config); + } + setup.config.functions = normalizeAndValidate(setup.config.functions); + const codebases = setup.config.functions.map((cfg: any) => clc.bold(cfg.codebase)); + logger.info(`\nDetected existing codebase(s): ${codebases.join(", ")}\n`); + const choices = [ + { + name: "Initialize", + value: "new", + }, + { + name: "Overwrite", + value: "overwrite", + }, + ]; + const initOpt = await promptOnce({ + type: "list", + message: "Would you like to initialize a new codebase, or overwrite an existing one?", + default: "new", + choices, + }); + return initOpt === "new" ? initNewCodebase(setup, config) : overwriteCodebase(setup, config); +} + +/** + * User dialogue to set up configuration for functions codebase. + */ +async function initNewCodebase(setup: any, config: Config): Promise { + logger.info("Let's create a new codebase for your functions."); + logger.info("A directory corresponding to the codebase will be created in your project"); + logger.info("with sample code pre-configured.\n"); + + logger.info("See https://firebase.google.com/docs/functions/organize-functions for"); + logger.info("more information on organizing your functions using codebases.\n"); + + logger.info(`Functions can be deployed with ${clc.bold("firebase deploy")}.\n`); + + let source: string; + let codebase: string; + + if (setup.config.functions.length === 0) { + source = "functions"; + codebase = "default"; + } else { + let attempts = 0; + while (true) { + if (attempts++ >= MAX_ATTEMPTS) { + throw new FirebaseError( + "Exceeded max number of attempts to input valid codebase name. Please restart.", + ); + } + codebase = await promptOnce({ + type: "input", + message: "What should be the name of this codebase?", + }); + try { + validateCodebase(codebase); + assertUnique(setup.config.functions, "codebase", codebase); + break; + } catch (err: any) { + logger.error(err as FirebaseError); + } + } + + attempts = 0; + while (true) { + if (attempts >= MAX_ATTEMPTS) { + throw new FirebaseError( + "Exceeded max number of attempts to input valid source. Please restart.", + ); + } + attempts++; + source = await promptOnce({ + type: "input", + message: `In what sub-directory would you like to initialize your functions for codebase ${clc.bold( + codebase, + )}?`, + default: codebase, + }); + try { + assertUnique(setup.config.functions, "source", source); + break; + } catch (err: any) { + logger.error(err as FirebaseError); + } + } + } + + setup.config.functions.push({ + source, + codebase, + }); + setup.functions.source = source; + setup.functions.codebase = codebase; + return languageSetup(setup, config); +} + +async function overwriteCodebase(setup: any, config: Config): Promise { + let codebase; + if (setup.config.functions.length > 1) { + const choices = setup.config.functions.map((cfg: any) => ({ + name: cfg["codebase"], + value: cfg["codebase"], + })); + codebase = await promptOnce({ + type: "list", + message: "Which codebase would you like to overwrite?", + choices, + }); + } else { + codebase = setup.config.functions[0].codebase; // only one codebase exists + } + + const cbconfig = configForCodebase(setup.config.functions, codebase); + setup.functions.source = cbconfig.source; + setup.functions.codebase = cbconfig.codebase; + + logger.info(`\nOverwriting ${clc.bold(`codebase ${codebase}...\n`)}`); + return languageSetup(setup, config); +} + +/** + * User dialogue to set up configuration for functions codebase language choice. + */ +async function languageSetup(setup: any, config: Config): Promise { + // During genkit setup, always select TypeScript here. + if (setup.languageOverride) { + return require("./" + setup.languageOverride).setup(setup, config); + } + const choices = [ { name: "JavaScript", @@ -36,17 +173,40 @@ module.exports = async function (setup: any, config: any, options: Options) { value: "typescript", }, ]; - if (previews.golang) { - choices.push({ - name: "Go", - value: "golang", - }); - } + choices.push({ + name: "Python", + value: "python", + }); const language = await promptOnce({ type: "list", message: "What language would you like to use to write Cloud Functions?", default: "javascript", choices, }); - return require("./" + language)(setup, config); -}; + const cbconfig = configForCodebase(setup.config.functions, setup.functions.codebase); + switch (language) { + case "javascript": + cbconfig.ignore = [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local", + ]; + break; + case "typescript": + cbconfig.ignore = [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local", + ]; + break; + case "python": + cbconfig.ignore = ["venv", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"]; + break; + } + setup.functions.languageChoice = language; + return require("./" + language).setup(setup, config); +} diff --git a/src/init/features/functions/javascript.js b/src/init/features/functions/javascript.js deleted file mode 100644 index 0766bfea40d..00000000000 --- a/src/init/features/functions/javascript.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var fs = require("fs"); -var path = require("path"); - -var npmDependencies = require("./npm-dependencies"); -var { prompt } = require("../../../prompt"); - -var TEMPLATE_ROOT = path.resolve(__dirname, "../../../../templates/init/functions/javascript/"); -var INDEX_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "index.js"), "utf8"); -var PACKAGE_LINTING_TEMPLATE = fs.readFileSync( - path.join(TEMPLATE_ROOT, "package.lint.json"), - "utf8" -); -var PACKAGE_NO_LINTING_TEMPLATE = fs.readFileSync( - path.join(TEMPLATE_ROOT, "package.nolint.json"), - "utf8" -); -var ESLINT_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "_eslintrc"), "utf8"); -var GITIGNORE_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "_gitignore"), "utf8"); - -module.exports = function (setup, config) { - return prompt(setup.functions, [ - { - name: "lint", - type: "confirm", - message: "Do you want to use ESLint to catch probable bugs and enforce style?", - default: false, - }, - ]) - .then(function () { - if (setup.functions.lint) { - _.set(setup, "config.functions.predeploy", ['npm --prefix "$RESOURCE_DIR" run lint']); - return config - .askWriteProjectFile("functions/package.json", PACKAGE_LINTING_TEMPLATE) - .then(function () { - config.askWriteProjectFile("functions/.eslintrc.js", ESLINT_TEMPLATE); - }); - } - return config.askWriteProjectFile("functions/package.json", PACKAGE_NO_LINTING_TEMPLATE); - }) - .then(function () { - return config.askWriteProjectFile("functions/index.js", INDEX_TEMPLATE); - }) - .then(function () { - return config.askWriteProjectFile("functions/.gitignore", GITIGNORE_TEMPLATE); - }) - .then(function () { - return npmDependencies.askInstallDependencies(setup.functions, config); - }); -}; diff --git a/src/init/features/functions/javascript.ts b/src/init/features/functions/javascript.ts new file mode 100644 index 00000000000..dea5c206161 --- /dev/null +++ b/src/init/features/functions/javascript.ts @@ -0,0 +1,47 @@ +import { askInstallDependencies } from "./npm-dependencies"; +import { prompt } from "../../../prompt"; +import { configForCodebase } from "../../../functions/projectConfig"; +import { readTemplateSync } from "../../../templates"; + +const INDEX_TEMPLATE = readTemplateSync("init/functions/javascript/index.js"); +const PACKAGE_LINTING_TEMPLATE = readTemplateSync("init/functions/javascript/package.lint.json"); +const PACKAGE_NO_LINTING_TEMPLATE = readTemplateSync( + "init/functions/javascript/package.nolint.json", +); +const ESLINT_TEMPLATE = readTemplateSync("init/functions/javascript/_eslintrc"); +const GITIGNORE_TEMPLATE = readTemplateSync("init/functions/javascript/_gitignore"); + +export function setup(setup: any, config: any): Promise { + return prompt(setup.functions, [ + { + name: "lint", + type: "confirm", + message: "Do you want to use ESLint to catch probable bugs and enforce style?", + default: false, + }, + ]) + .then(() => { + if (setup.functions.lint) { + const cbconfig = configForCodebase(setup.config.functions, setup.functions.codebase); + cbconfig.predeploy = ['npm --prefix "$RESOURCE_DIR" run lint']; + return config + .askWriteProjectFile(`${setup.functions.source}/package.json`, PACKAGE_LINTING_TEMPLATE) + .then(() => { + config.askWriteProjectFile(`${setup.functions.source}/.eslintrc.js`, ESLINT_TEMPLATE); + }); + } + return config.askWriteProjectFile( + `${setup.functions.source}/package.json`, + PACKAGE_NO_LINTING_TEMPLATE, + ); + }) + .then(() => { + return config.askWriteProjectFile(`${setup.functions.source}/index.js`, INDEX_TEMPLATE); + }) + .then(() => { + return config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE); + }) + .then(() => { + return askInstallDependencies(setup.functions, config); + }); +} diff --git a/src/init/features/functions/npm-dependencies.js b/src/init/features/functions/npm-dependencies.js deleted file mode 100644 index e8c9588bccf..00000000000 --- a/src/init/features/functions/npm-dependencies.js +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; - -var spawn = require("cross-spawn"); - -const { logger } = require("../../../logger"); -var { prompt } = require("../../../prompt"); - -exports.askInstallDependencies = function (setup, config) { - return prompt(setup, [ - { - name: "npm", - type: "confirm", - message: "Do you want to install dependencies with npm now?", - default: true, - }, - ]).then(function () { - if (setup.npm) { - return new Promise(function (resolve) { - var installer = spawn("npm", ["install"], { - cwd: config.projectDir + "/functions", - stdio: "inherit", - }); - - installer.on("error", function (err) { - logger.debug(err.stack); - }); - - installer.on("close", function (code) { - if (code === 0) { - return resolve(); - } - logger.info(); - logger.error("NPM install failed, continuing with Firebase initialization..."); - return resolve(); - }); - }); - } - }); -}; diff --git a/src/init/features/functions/npm-dependencies.ts b/src/init/features/functions/npm-dependencies.ts new file mode 100644 index 00000000000..fb7d7246123 --- /dev/null +++ b/src/init/features/functions/npm-dependencies.ts @@ -0,0 +1,22 @@ +import { logger } from "../../../logger"; +import { prompt } from "../../../prompt"; +import { wrapSpawn } from "../../spawn"; + +export async function askInstallDependencies(setup: any, config: any): Promise { + await prompt(setup, [ + { + name: "npm", + type: "confirm", + message: "Do you want to install dependencies with npm now?", + default: true, + }, + ]); + if (setup.npm) { + try { + await wrapSpawn("npm", ["install"], config.projectDir + `/${setup.source}`); + } catch (e) { + logger.info(); + logger.error("NPM install failed, continuing with Firebase initialization..."); + } + } +} diff --git a/src/init/features/functions/python.ts b/src/init/features/functions/python.ts new file mode 100644 index 00000000000..2475202f358 --- /dev/null +++ b/src/init/features/functions/python.ts @@ -0,0 +1,70 @@ +import * as spawn from "cross-spawn"; + +import { Config } from "../../../config"; +import { getPythonBinary } from "../../../deploy/functions/runtimes/python"; +import { runWithVirtualEnv } from "../../../functions/python"; +import { promptOnce } from "../../../prompt"; +import { latest } from "../../../deploy/functions/runtimes/supported"; +import { readTemplateSync } from "../../../templates"; + +const MAIN_TEMPLATE = readTemplateSync("init/functions/python/main.py"); +const REQUIREMENTS_TEMPLATE = readTemplateSync("init/functions/python/requirements.txt"); +const GITIGNORE_TEMPLATE = readTemplateSync("init/functions/python/_gitignore"); + +/** + * Create a Python Firebase Functions project. + */ +export async function setup(setup: any, config: Config): Promise { + await config.askWriteProjectFile( + `${setup.functions.source}/requirements.txt`, + REQUIREMENTS_TEMPLATE, + ); + await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE); + await config.askWriteProjectFile(`${setup.functions.source}/main.py`, MAIN_TEMPLATE); + + // Write the latest supported runtime version to the config. + config.set("functions.runtime", latest("python")); + // Add python specific ignores to config. + config.set("functions.ignore", ["venv", "__pycache__"]); + + // Setup VENV. + const venvProcess = spawn(getPythonBinary(latest("python")), ["-m", "venv", "venv"], { + shell: true, + cwd: config.path(setup.functions.source), + stdio: [/* stdin= */ "pipe", /* stdout= */ "pipe", /* stderr= */ "pipe", "pipe"], + }); + await new Promise((resolve, reject) => { + venvProcess.on("exit", resolve); + venvProcess.on("error", reject); + }); + + const install = await promptOnce({ + name: "install", + type: "confirm", + message: "Do you want to install dependencies now?", + default: true, + }); + if (install) { + // Update pip to support dependencies like pyyaml. + const upgradeProcess = runWithVirtualEnv( + ["pip3", "install", "--upgrade", "pip"], + config.path(setup.functions.source), + {}, + { stdio: ["inherit", "inherit", "inherit"] }, + ); + await new Promise((resolve, reject) => { + upgradeProcess.on("exit", resolve); + upgradeProcess.on("error", reject); + }); + const installProcess = runWithVirtualEnv( + [getPythonBinary(latest("python")), "-m", "pip", "install", "-r", "requirements.txt"], + config.path(setup.functions.source), + {}, + { stdio: ["inherit", "inherit", "inherit"] }, + ); + await new Promise((resolve, reject) => { + installProcess.on("exit", resolve); + installProcess.on("error", reject); + }); + } +} diff --git a/src/init/features/functions/typescript.js b/src/init/features/functions/typescript.js deleted file mode 100644 index 5bdb51a45a6..00000000000 --- a/src/init/features/functions/typescript.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var fs = require("fs"); -var path = require("path"); - -var npmDependencies = require("./npm-dependencies"); -var { prompt } = require("../../../prompt"); - -var TEMPLATE_ROOT = path.resolve(__dirname, "../../../../templates/init/functions/typescript/"); -var PACKAGE_LINTING_TEMPLATE = fs.readFileSync( - path.join(TEMPLATE_ROOT, "package.lint.json"), - "utf8" -); -var PACKAGE_NO_LINTING_TEMPLATE = fs.readFileSync( - path.join(TEMPLATE_ROOT, "package.nolint.json"), - "utf8" -); -var ESLINT_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "_eslintrc"), "utf8"); -var TSCONFIG_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "tsconfig.json"), "utf8"); -var TSCONFIG_DEV_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "tsconfig.dev.json"), "utf8"); -var INDEX_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "index.ts"), "utf8"); -var GITIGNORE_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "_gitignore"), "utf8"); - -module.exports = function (setup, config) { - return prompt(setup.functions, [ - { - name: "lint", - type: "confirm", - message: "Do you want to use ESLint to catch probable bugs and enforce style?", - default: true, - }, - ]) - .then(function () { - if (setup.functions.lint) { - _.set(setup, "config.functions.predeploy", [ - 'npm --prefix "$RESOURCE_DIR" run lint', - 'npm --prefix "$RESOURCE_DIR" run build', - ]); - return config - .askWriteProjectFile("functions/package.json", PACKAGE_LINTING_TEMPLATE) - .then(function () { - return config.askWriteProjectFile("functions/.eslintrc.js", ESLINT_TEMPLATE); - }); - } - _.set(setup, "config.functions.predeploy", 'npm --prefix "$RESOURCE_DIR" run build'); - return config.askWriteProjectFile("functions/package.json", PACKAGE_NO_LINTING_TEMPLATE); - }) - .then(function () { - return config.askWriteProjectFile("functions/tsconfig.json", TSCONFIG_TEMPLATE); - }) - .then(function () { - if (setup.functions.lint) { - return config.askWriteProjectFile("functions/tsconfig.dev.json", TSCONFIG_DEV_TEMPLATE); - } - }) - .then(function () { - return config.askWriteProjectFile("functions/src/index.ts", INDEX_TEMPLATE); - }) - .then(function () { - return config.askWriteProjectFile("functions/.gitignore", GITIGNORE_TEMPLATE); - }) - .then(function () { - return npmDependencies.askInstallDependencies(setup.functions, config); - }); -}; diff --git a/src/init/features/functions/typescript.ts b/src/init/features/functions/typescript.ts new file mode 100644 index 00000000000..2e9a4853cd1 --- /dev/null +++ b/src/init/features/functions/typescript.ts @@ -0,0 +1,70 @@ +import { askInstallDependencies } from "./npm-dependencies"; +import { prompt } from "../../../prompt"; +import { configForCodebase } from "../../../functions/projectConfig"; +import { readTemplateSync } from "../../../templates"; + +const PACKAGE_LINTING_TEMPLATE = readTemplateSync("init/functions/typescript/package.lint.json"); +const PACKAGE_NO_LINTING_TEMPLATE = readTemplateSync( + "init/functions/typescript/package.nolint.json", +); +const ESLINT_TEMPLATE = readTemplateSync("init/functions/typescript/_eslintrc"); +const TSCONFIG_TEMPLATE = readTemplateSync("init/functions/typescript/tsconfig.json"); +const TSCONFIG_DEV_TEMPLATE = readTemplateSync("init/functions/typescript/tsconfig.dev.json"); +const INDEX_TEMPLATE = readTemplateSync("init/functions/typescript/index.ts"); +const GITIGNORE_TEMPLATE = readTemplateSync("init/functions/typescript/_gitignore"); + +export function setup(setup: any, config: any): Promise { + return prompt(setup.functions, [ + { + name: "lint", + type: "confirm", + message: "Do you want to use ESLint to catch probable bugs and enforce style?", + default: true, + }, + ]) + .then(() => { + const cbconfig = configForCodebase(setup.config.functions, setup.functions.codebase); + cbconfig.predeploy = []; + if (setup.functions.lint) { + cbconfig.predeploy.push('npm --prefix "$RESOURCE_DIR" run lint'); + cbconfig.predeploy.push('npm --prefix "$RESOURCE_DIR" run build'); + return config + .askWriteProjectFile(`${setup.functions.source}/package.json`, PACKAGE_LINTING_TEMPLATE) + .then(() => { + return config.askWriteProjectFile( + `${setup.functions.source}/.eslintrc.js`, + ESLINT_TEMPLATE, + ); + }); + } else { + cbconfig.predeploy.push('npm --prefix "$RESOURCE_DIR" run build'); + } + return config.askWriteProjectFile( + `${setup.functions.source}/package.json`, + PACKAGE_NO_LINTING_TEMPLATE, + ); + }) + .then(() => { + return config.askWriteProjectFile( + `${setup.functions.source}/tsconfig.json`, + TSCONFIG_TEMPLATE, + ); + }) + .then(() => { + if (setup.functions.lint) { + return config.askWriteProjectFile( + `${setup.functions.source}/tsconfig.dev.json`, + TSCONFIG_DEV_TEMPLATE, + ); + } + }) + .then(() => { + return config.askWriteProjectFile(`${setup.functions.source}/src/index.ts`, INDEX_TEMPLATE); + }) + .then(() => { + return config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE); + }) + .then(() => { + return askInstallDependencies(setup.functions, config); + }); +} diff --git a/src/init/features/genkit.ts b/src/init/features/genkit.ts new file mode 100644 index 00000000000..a7c5148a89b --- /dev/null +++ b/src/init/features/genkit.ts @@ -0,0 +1,59 @@ +import { logger } from "../../logger"; +import { doSetup as functionsSetup } from "./functions"; +import { Options } from "../../options"; +import { Config } from "../../config"; +import { promptOnce } from "../../prompt"; +import { wrapSpawn } from "../spawn"; + +/** + * doSetup is the entry point for setting up the genkit suite. + */ +export async function doSetup(setup: any, config: Config, options: Options): Promise { + if (setup.functions?.languageChoice !== "typescript") { + const continueFunctions = await promptOnce({ + type: "confirm", + message: + "Genkit's Firebase integration uses Cloud Functions for Firebase with TypeScript. Initialize Functions to continue?", + default: true, + }); + if (!continueFunctions) { + logger.info("Stopped Genkit initialization"); + return; + } + + // Functions with genkit should always be typescript + setup.languageOverride = "typescript"; + await functionsSetup(setup, config, options); + delete setup.languageOverride; + logger.info(); + } + + const projectDir: string = `${config.projectDir}/${setup.functions.source}`; + + const installType = await promptOnce({ + type: "list", + message: "Install the Genkit CLI globally or locally in this project?", + choices: [ + { name: "Globally", value: "globally" }, + { name: "Just this project", value: "project" }, + ], + }); + + try { + logger.info("Installing Genkit CLI"); + if (installType === "globally") { + await wrapSpawn("npm", ["install", "-g", "genkit"], projectDir); + await wrapSpawn("genkit", ["init", "-p", "firebase"], projectDir); + logger.info("Start the Genkit developer experience by running:"); + logger.info(` cd ${setup.functions.source} && genkit start`); + } else { + await wrapSpawn("npm", ["install", "genkit", "--save-dev"], projectDir); + await wrapSpawn("npx", ["genkit", "init", "-p", "firebase"], projectDir); + logger.info("Start the Genkit developer experience by running:"); + logger.info(` cd ${setup.functions.source} && npx genkit start`); + } + } catch (e) { + logger.error("Genkit initialization failed..."); + return; + } +} diff --git a/src/init/features/hosting/github.ts b/src/init/features/hosting/github.ts index e54c6c53feb..46bf98cd7f6 100644 --- a/src/init/features/hosting/github.ts +++ b/src/init/features/hosting/github.ts @@ -1,10 +1,9 @@ -import { bold } from "cli-color"; +import { bold, underline } from "colorette"; import * as fs from "fs"; -import * as yaml from "js-yaml"; -import { safeLoad } from "js-yaml"; +import * as yaml from "yaml"; import * as ora from "ora"; import * as path from "path"; -import * as sodium from "tweetsodium"; +import * as libsodium from "libsodium-wrappers"; import { Setup } from "../.."; import { loginGithub } from "../../../auth"; @@ -13,6 +12,7 @@ import { createServiceAccount, createServiceAccountKey, deleteServiceAccount, + listServiceAccountKeys, } from "../../../gcp/iam"; import { addServiceAccountToRoles, firebaseRoles } from "../../../gcp/resourceManager"; import { logger } from "../../../logger"; @@ -20,6 +20,7 @@ import { prompt } from "../../../prompt"; import { logBullet, logLabeledBullet, logSuccess, logWarning, reject } from "../../../utils"; import { githubApiOrigin, githubClientId } from "../../../api"; import { Client } from "../../../apiv2"; +import { FirebaseError } from "../../../error"; let GIT_DIR: string; let GITHUB_DIR: string; @@ -30,10 +31,12 @@ let YML_FULL_PATH_MERGE: string; const YML_PULL_REQUEST_FILENAME = "firebase-hosting-pull-request.yml"; const YML_MERGE_FILENAME = "firebase-hosting-merge.yml"; -const CHECKOUT_GITHUB_ACTION_NAME = "actions/checkout@v2"; +const CHECKOUT_GITHUB_ACTION_NAME = "actions/checkout@v4"; const HOSTING_GITHUB_ACTION_NAME = "FirebaseExtended/action-hosting-deploy@v0"; -const githubApiClient = new Client({ urlPrefix: githubApiOrigin, auth: false }); +const SERVICE_ACCOUNT_MAX_KEY_NUMBER = 10; + +const githubApiClient = new Client({ urlPrefix: githubApiOrigin(), auth: false }); /** * Assists in setting up a GitHub workflow by doing the following: @@ -49,14 +52,14 @@ const githubApiClient = new Client({ urlPrefix: githubApiOrigin, auth: false }); * @param config Configuration for the project. * @param options Command line options. */ -export async function initGitHub(setup: Setup, config: any, options: any): Promise { +export async function initGitHub(setup: Setup): Promise { if (!setup.projectId) { return reject("Could not determine Project ID, can't set up GitHub workflow.", { exit: 1 }); } if (!setup.config.hosting) { return reject( - `Didn't find a Hosting config in firebase.json. Run ${bold("firebase init hosting")} instead.` + `Didn't find a Hosting config in firebase.json. Run ${bold("firebase init hosting")} instead.`, ); } @@ -72,7 +75,7 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi // GitHub Oauth logBullet( - "Authorizing with GitHub to upload your service account to a GitHub repository's secrets store." + "Authorizing with GitHub to upload your service account to a GitHub repository's secrets store.", ); const ghAccessToken = await signInWithGitHub(); @@ -101,15 +104,15 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi const serviceAccountJSON = await createServiceAccountAndKeyWithRetry( setup, repo, - serviceAccountName + serviceAccountName, ); logger.info(); logSuccess( - `Created service account ${bold(serviceAccountName)} with Firebase Hosting admin permissions.` + `Created service account ${bold(serviceAccountName)} with Firebase Hosting admin permissions.`, ); - const spinnerSecrets = ora.default(`Uploading service account secrets to repository: ${repo}`); + const spinnerSecrets = ora(`Uploading service account secrets to repository: ${repo}`); spinnerSecrets.start(); const encryptedServiceAccountJSON = encryptServiceAccountJSON(serviceAccountJSON, key); @@ -117,9 +120,9 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi await uploadSecretToGitHub( repo, ghAccessToken, - encryptedServiceAccountJSON, + await encryptedServiceAccountJSON, keyId, - githubSecretName + githubSecretName, ); spinnerSecrets.stop(); @@ -154,7 +157,7 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi YML_FULL_PATH_PULL_REQUEST, githubSecretName, setup.projectId, - script + script, ); logger.info(); @@ -188,7 +191,7 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi branch, githubSecretName, setup.projectId, - script + script, ); logger.info(); @@ -199,10 +202,10 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi logger.info(); logLabeledBullet( "Action required", - `Visit this URL to revoke authorization for the Firebase CLI GitHub OAuth App:` + `Visit this URL to revoke authorization for the Firebase CLI GitHub OAuth App:`, ); logger.info( - bold.underline(`https://github.com/settings/connections/applications/${githubClientId}`) + bold(underline(`https://github.com/settings/connections/applications/${githubClientId()}`)), ); logLabeledBullet("Action required", `Push any new workflow file(s) to your repo`); } @@ -260,7 +263,7 @@ function loadYMLDeploy(): { exists: boolean; branch?: string } { } function loadYML(ymlPath: string) { - return safeLoad(fs.readFileSync(ymlPath, "utf8")); + return yaml.parse(fs.readFileSync(ymlPath, "utf8")); } function mkdirNotExists(dir: string): void { @@ -273,6 +276,7 @@ function mkdirNotExists(dir: string): void { type GitHubWorkflowConfig = { name: string; on: string | { [key: string]: { [key: string]: string[] } }; + permissions?: string | { [key: string]: string }; jobs: { [key: string]: { if?: string; @@ -291,11 +295,16 @@ function writeChannelActionYMLFile( ymlPath: string, secretName: string, projectId: string, - script?: string + script?: string, ): void { const workflowConfig: GitHubWorkflowConfig = { name: "Deploy to Firebase Hosting on PR", on: "pull_request", + permissions: { + checks: "write", + contents: "read", + "pull-requests": "write", + }, jobs: { ["build_and_preview"]: { if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}", // secrets aren't accessible on PRs from forks @@ -323,7 +332,7 @@ function writeChannelActionYMLFile( const ymlContents = `# This file was auto-generated by the Firebase CLI # https://github.com/firebase/firebase-tools -${yaml.safeDump(workflowConfig)}`; +${yaml.stringify(workflowConfig)}`; mkdirNotExists(GITHUB_DIR); mkdirNotExists(WORKFLOW_DIR); @@ -335,7 +344,7 @@ function writeDeployToProdActionYMLFile( branch: string | undefined, secretName: string, projectId: string, - script?: string + script?: string, ): void { const workflowConfig: GitHubWorkflowConfig = { name: "Deploy to Firebase Hosting on merge", @@ -367,7 +376,7 @@ function writeDeployToProdActionYMLFile( const ymlContents = `# This file was auto-generated by the Firebase CLI # https://github.com/firebase/firebase-tools -${yaml.safeDump(workflowConfig)}`; +${yaml.stringify(workflowConfig)}`; mkdirNotExists(GITHUB_DIR); mkdirNotExists(WORKFLOW_DIR); @@ -379,7 +388,7 @@ async function uploadSecretToGitHub( ghAccessToken: string, encryptedServiceAccountJSON: string, keyId: string, - secretName: string + secretName: string, ): Promise<{ status: any }> { const data = { ["encrypted_value"]: encryptedServiceAccountJSON, @@ -389,13 +398,13 @@ async function uploadSecretToGitHub( return await githubApiClient.put( `/repos/${repo}/actions/secrets/${secretName}`, data, - { headers } + { headers }, ); } async function promptForRepo( options: any, - ghAccessToken: string + ghAccessToken: string, ): Promise<{ repo: string; key: string; keyId: string }> { let key = ""; let keyId = ""; @@ -408,33 +417,34 @@ async function promptForRepo( "For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository)", validate: async (repo: string) => { try { - // eslint-disable-next-line camelcase const { body } = await githubApiClient.get<{ key: string; key_id: string }>( `/repos/${repo}/actions/secrets/public-key`, { headers: { Authorization: `token ${ghAccessToken}`, "User-Agent": "Firebase CLI" }, queryParams: { type: "owner" }, - } + }, ); key = body.key; keyId = body.key_id; - } catch (e) { - if (e.status === 403) { + } catch (e: any) { + if ([403, 404].includes(e.status)) { logger.info(); logger.info(); logWarning( "The provided authorization cannot be used with this repository. If this repository is in an organization, did you remember to grant access?", - "error" + "error", ); logger.info(); logLabeledBullet( "Action required", - `Visit this URL to ensure access has been granted to the appropriate organization(s) for the Firebase CLI GitHub OAuth App:` + `Visit this URL to ensure access has been granted to the appropriate organization(s) for the Firebase CLI GitHub OAuth App:`, ); logger.info( - bold.underline( - `https://github.com/settings/connections/applications/${githubClientId}` - ) + bold( + underline( + `https://github.com/settings/connections/applications/${githubClientId()}`, + ), + ), ); logger.info(); } @@ -474,7 +484,7 @@ async function promptForBuildScript(): Promise<{ script?: string }> { } async function promptToSetupDeploys( - defaultBranch: string + defaultBranch: string, ): Promise<{ setupDeploys: boolean; branch?: string }> { const { setupDeploys } = await prompt({}, [ { @@ -520,12 +530,11 @@ async function getGitHubUserDetails(ghAccessToken: any): Promise( `/repos/${repo}`, { headers: { Authorization: `token ${ghAccessToken}`, "User-Agent": "Firebase CLI" }, - } + }, ); return body; } @@ -537,18 +546,30 @@ async function signInWithGitHub() { async function createServiceAccountAndKeyWithRetry( options: any, repo: string, - accountId: string + accountId: string, ): Promise { - const spinnerServiceAccount = ora.default("Retrieving a service account."); + const spinnerServiceAccount = ora("Retrieving a service account."); spinnerServiceAccount.start(); try { const serviceAccountJSON = await createServiceAccountAndKey(options, repo, accountId); spinnerServiceAccount.stop(); return serviceAccountJSON; - } catch (e) { + } catch (e: any) { spinnerServiceAccount.stop(); if (!e.message.includes("429")) { + const serviceAccountKeys = await listServiceAccountKeys(options.projectId, accountId); + if (serviceAccountKeys.length >= SERVICE_ACCOUNT_MAX_KEY_NUMBER) { + throw new FirebaseError( + `You cannot add another key because the service account ${bold( + accountId, + )} already contains the max number of keys: ${SERVICE_ACCOUNT_MAX_KEY_NUMBER}.`, + { + original: e, + exit: 1, + }, + ); + } throw e; } @@ -556,7 +577,7 @@ async function createServiceAccountAndKeyWithRetry( spinnerServiceAccount.start(); await deleteServiceAccount( options.projectId, - `${accountId}@${options.projectId}.iam.gserviceaccount.com` + `${accountId}@${options.projectId}.iam.gserviceaccount.com`, ); const serviceAccountJSON = await createServiceAccountAndKey(options, repo, accountId); spinnerServiceAccount.stop(); @@ -567,16 +588,16 @@ async function createServiceAccountAndKeyWithRetry( async function createServiceAccountAndKey( options: any, repo: string, - accountId: string + accountId: string, ): Promise { try { await createServiceAccount( options.projectId, accountId, - `A service account with permission to deploy to Firebase Hosting for the GitHub repository ${repo}`, - `GitHub Actions (${repo})` + `A service account with permission to deploy to Firebase Hosting and Cloud Functions for the GitHub repository ${repo}`, + `GitHub Actions (${repo})`, ); - } catch (e) { + } catch (e: any) { // No need to throw if there is an existing service account if (!e.message.includes("409")) { throw e; @@ -589,6 +610,10 @@ async function createServiceAccountAndKey( // https://github.com/firebase/firebase-tools/issues/2732 firebaseRoles.authAdmin, + // Required to add preview URLs to Auth authorized domains + // https://github.com/firebase/firebase-tools/issues/6828 + firebaseRoles.serviceUsageConsumer, + // Required for CLI deploys firebaseRoles.apiKeysViewer, @@ -597,6 +622,9 @@ async function createServiceAccountAndKey( // Required for projects that use Hosting rewrites to Cloud Run firebaseRoles.runViewer, + + // Required for previewing backends (Web Frameworks and pinTags) + firebaseRoles.functionsDeveloper, ]; await addServiceAccountToRoles(options.projectId, accountId, requiredRoles); @@ -622,15 +650,20 @@ async function createServiceAccountAndKey( * @param serviceAccountJSON A service account's JSON private key * @param key a GitHub repository's public key * - * @return {string} The encrypted service account key + * @return The encrypted service account key */ -function encryptServiceAccountJSON(serviceAccountJSON: string, key: string): string { +async function encryptServiceAccountJSON(serviceAccountJSON: string, key: string): Promise { const messageBytes = Buffer.from(serviceAccountJSON); const keyBytes = Buffer.from(key, "base64"); // Encrypt using LibSodium. - const encryptedBytes = sodium.seal(messageBytes, keyBytes); + await libsodium.ready; + const encryptedBytes = libsodium.crypto_box_seal(messageBytes, keyBytes); // Base64 the encrypted secret return Buffer.from(encryptedBytes).toString("base64"); } + +export function isRunningInGithubAction() { + return process.env.GITHUB_ACTION_REPOSITORY === HOSTING_GITHUB_ACTION_NAME.split("@")[0]; +} diff --git a/src/init/features/hosting/index.js b/src/init/features/hosting/index.js deleted file mode 100644 index cf6243a7469..00000000000 --- a/src/init/features/hosting/index.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; - -const clc = require("cli-color"); -const fs = require("fs"); - -const { Client } = require("../../../apiv2"); -const { initGitHub } = require("./github"); -const { prompt } = require("../../../prompt"); -const { logger } = require("../../../logger"); - -const INDEX_TEMPLATE = fs.readFileSync( - __dirname + "/../../../../templates/init/hosting/index.html", - "utf8" -); -const MISSING_TEMPLATE = fs.readFileSync( - __dirname + "/../../../../templates/init/hosting/404.html", - "utf8" -); -const DEFAULT_IGNORES = ["firebase.json", "**/.*", "**/node_modules/**"]; - -module.exports = function (setup, config, options) { - setup.hosting = {}; - - logger.info(); - logger.info( - "Your " + - clc.bold("public") + - " directory is the folder (relative to your project directory) that" - ); - logger.info( - "will contain Hosting assets to be uploaded with " + clc.bold("firebase deploy") + ". If you" - ); - logger.info("have a build process for your assets, use your build's output directory."); - logger.info(); - - return prompt(setup.hosting, [ - { - name: "public", - type: "input", - default: "public", - message: "What do you want to use as your public directory?", - }, - { - name: "spa", - type: "confirm", - default: false, - message: "Configure as a single-page app (rewrite all urls to /index.html)?", - }, - { - name: "github", - type: "confirm", - default: false, - message: "Set up automatic builds and deploys with GitHub?", - }, - ]).then(function () { - setup.config.hosting = { - public: setup.hosting.public, - ignore: DEFAULT_IGNORES, - }; - - let next; - if (setup.hosting.spa) { - setup.config.hosting.rewrites = [{ source: "**", destination: "/index.html" }]; - next = Promise.resolve(); - } else { - // SPA doesn't need a 404 page since everything is index.html - next = config.askWriteProjectFile(setup.hosting.public + "/404.html", MISSING_TEMPLATE); - } - - return next - .then(() => { - const c = new Client({ urlPrefix: "https://www.gstatic.com", auth: false }); - return c.get("/firebasejs/releases.json"); - }) - .then((response) => { - return config.askWriteProjectFile( - setup.hosting.public + "/index.html", - INDEX_TEMPLATE.replace(/{{VERSION}}/g, response.body.current.version) - ); - }) - .then(() => { - if (setup.hosting.github) { - return initGitHub(setup, config, options); - } - }); - }); -}; diff --git a/src/init/features/hosting/index.ts b/src/init/features/hosting/index.ts new file mode 100644 index 00000000000..eda8d2f63f0 --- /dev/null +++ b/src/init/features/hosting/index.ts @@ -0,0 +1,231 @@ +import * as clc from "colorette"; +import { sync as rimraf } from "rimraf"; +import { join } from "path"; + +import { Client } from "../../../apiv2"; +import { initGitHub } from "./github"; +import { prompt, promptOnce } from "../../../prompt"; +import { logger } from "../../../logger"; +import { discover, WebFrameworks } from "../../../frameworks"; +import { ALLOWED_SSR_REGIONS, DEFAULT_REGION } from "../../../frameworks/constants"; +import * as experiments from "../../../experiments"; +import { errNoDefaultSite, getDefaultHostingSite } from "../../../getDefaultHostingSite"; +import { Options } from "../../../options"; +import { last, logSuccess } from "../../../utils"; +import { interactiveCreateHostingSite } from "../../../hosting/interactive"; +import { readTemplateSync } from "../../../templates"; + +const INDEX_TEMPLATE = readTemplateSync("init/hosting/index.html"); +const MISSING_TEMPLATE = readTemplateSync("init/hosting/404.html"); +const DEFAULT_IGNORES = ["firebase.json", "**/.*", "**/node_modules/**"]; + +/** + * Does the setup steps for Firebase Hosting. + * WARNING: #6527 - `options` may not have all the things you think it does. + */ +export async function doSetup(setup: any, config: any, options: Options): Promise { + setup.hosting = {}; + + // There's a path where we can set up Hosting without a project, so if + // if setup.projectId is empty, we don't do any checking for a Hosting site. + if (setup.projectId) { + let hasHostingSite = true; + try { + await getDefaultHostingSite({ projectId: setup.projectId }); + } catch (err: unknown) { + if (err !== errNoDefaultSite) { + throw err; + } + hasHostingSite = false; + } + + if (!hasHostingSite) { + const confirmCreate = await promptOnce({ + type: "confirm", + message: "A Firebase Hosting site is required to deploy. Would you like to create one now?", + default: true, + }); + if (confirmCreate) { + const createOptions = { + projectId: setup.projectId, + nonInteractive: options.nonInteractive, + }; + const newSite = await interactiveCreateHostingSite("", "", createOptions); + logger.info(); + logSuccess(`Firebase Hosting site ${last(newSite.name.split("/"))} created!`); + logger.info(); + } + } + } + + let discoveredFramework = experiments.isEnabled("webframeworks") + ? await discover(config.projectDir, false) + : undefined; + + if (experiments.isEnabled("webframeworks")) { + if (discoveredFramework) { + const name = WebFrameworks[discoveredFramework.framework].name; + await promptOnce( + { + name: "useDiscoveredFramework", + type: "confirm", + default: true, + message: `Detected an existing ${name} codebase in the current directory, should we use this?`, + }, + setup.hosting, + ); + } + if (setup.hosting.useDiscoveredFramework) { + setup.hosting.source = "."; + setup.hosting.useWebFrameworks = true; + } else { + await promptOnce( + { + name: "useWebFrameworks", + type: "confirm", + default: false, + message: `Do you want to use a web framework? (${clc.bold("experimental")})`, + }, + setup.hosting, + ); + } + } + + if (setup.hosting.useWebFrameworks) { + await promptOnce( + { + name: "source", + type: "input", + default: "hosting", + message: "What folder would you like to use for your web application's root directory?", + }, + setup.hosting, + ); + + if (setup.hosting.source !== ".") delete setup.hosting.useDiscoveredFramework; + discoveredFramework = await discover(join(config.projectDir, setup.hosting.source)); + + if (discoveredFramework) { + const name = WebFrameworks[discoveredFramework.framework].name; + await promptOnce( + { + name: "useDiscoveredFramework", + type: "confirm", + default: true, + message: `Detected an existing ${name} codebase in ${setup.hosting.source}, should we use this?`, + }, + setup.hosting, + ); + } + + if (setup.hosting.useDiscoveredFramework && discoveredFramework) { + setup.hosting.webFramework = discoveredFramework.framework; + } else { + const choices: { name: string; value: string }[] = []; + for (const value in WebFrameworks) { + if (WebFrameworks[value]) { + const { name, init } = WebFrameworks[value]; + if (init) choices.push({ name, value }); + } + } + + const defaultChoice = choices.find( + ({ value }) => value === discoveredFramework?.framework, + )?.value; + + await promptOnce( + { + name: "whichFramework", + type: "list", + message: "Please choose the framework:", + default: defaultChoice, + choices, + }, + setup.hosting, + ); + + if (discoveredFramework) rimraf(setup.hosting.source); + await WebFrameworks[setup.hosting.whichFramework].init!(setup, config); + } + + await promptOnce( + { + name: "region", + type: "list", + message: "In which region would you like to host server-side content, if applicable?", + default: DEFAULT_REGION, + choices: ALLOWED_SSR_REGIONS.filter((region) => region.recommended), + }, + setup.hosting, + ); + + setup.config.hosting = { + source: setup.hosting.source, + // TODO swap out for framework ignores + ignore: DEFAULT_IGNORES, + frameworksBackend: { + region: setup.hosting.region, + }, + }; + } else { + logger.info(); + logger.info( + `Your ${clc.bold("public")} directory is the folder (relative to your project directory) that`, + ); + logger.info( + `will contain Hosting assets to be uploaded with ${clc.bold("firebase deploy")}. If you`, + ); + logger.info("have a build process for your assets, use your build's output directory."); + logger.info(); + + await prompt(setup.hosting, [ + { + name: "public", + type: "input", + default: "public", + message: "What do you want to use as your public directory?", + }, + { + name: "spa", + type: "confirm", + default: false, + message: "Configure as a single-page app (rewrite all urls to /index.html)?", + }, + ]); + + setup.config.hosting = { + public: setup.hosting.public, + ignore: DEFAULT_IGNORES, + }; + } + + await promptOnce( + { + name: "github", + type: "confirm", + default: false, + message: "Set up automatic builds and deploys with GitHub?", + }, + setup.hosting, + ); + + if (!setup.hosting.useWebFrameworks) { + if (setup.hosting.spa) { + setup.config.hosting.rewrites = [{ source: "**", destination: "/index.html" }]; + } else { + // SPA doesn't need a 404 page since everything is index.html + await config.askWriteProjectFile(`${setup.hosting.public}/404.html`, MISSING_TEMPLATE); + } + + const c = new Client({ urlPrefix: "https://www.gstatic.com", auth: false }); + const response = await c.get<{ current: { version: string } }>("/firebasejs/releases.json"); + await config.askWriteProjectFile( + `${setup.hosting.public}/index.html`, + INDEX_TEMPLATE.replace(/{{VERSION}}/g, response.body.current.version), + ); + } + + if (setup.hosting.github) { + return initGitHub(setup); + } +} diff --git a/src/init/features/index.js b/src/init/features/index.js deleted file mode 100644 index c490bb0b7e9..00000000000 --- a/src/init/features/index.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; - -module.exports = { - account: require("./account").doSetup, - database: require("./database").doSetup, - firestore: require("./firestore").doSetup, - functions: require("./functions"), - hosting: require("./hosting"), - storage: require("./storage").doSetup, - emulators: require("./emulators").doSetup, - // always runs, sets up .firebaserc - project: require("./project").doSetup, - remoteconfig: require("./remoteconfig").doSetup, - "hosting:github": require("./hosting/github").initGitHub, -}; diff --git a/src/init/features/index.ts b/src/init/features/index.ts new file mode 100644 index 00000000000..9673ab64206 --- /dev/null +++ b/src/init/features/index.ts @@ -0,0 +1,16 @@ +export { doSetup as account } from "./account"; +export { doSetup as database } from "./database"; +export { doSetup as firestore } from "./firestore"; +export { doSetup as functions } from "./functions"; +export { doSetup as hosting } from "./hosting"; +export { doSetup as storage } from "./storage"; +export { doSetup as emulators } from "./emulators"; +export { doSetup as extensions } from "./extensions"; +// always runs, sets up .firebaserc +export { doSetup as project } from "./project"; +export { doSetup as remoteconfig } from "./remoteconfig"; +export { initGitHub as hostingGithub } from "./hosting/github"; +export { doSetup as dataconnect } from "./dataconnect"; +export { doSetup as dataconnectSdk } from "./dataconnect/sdk"; +export { doSetup as apphosting } from "../../apphosting"; +export { doSetup as genkit } from "./genkit"; diff --git a/src/test/init/features/project.spec.ts b/src/init/features/project.spec.ts similarity index 95% rename from src/test/init/features/project.spec.ts rename to src/init/features/project.spec.ts index 3a525b13077..394f18d5359 100644 --- a/src/test/init/features/project.spec.ts +++ b/src/init/features/project.spec.ts @@ -1,14 +1,15 @@ import { expect } from "chai"; import * as _ from "lodash"; import * as sinon from "sinon"; -import { configstore } from "../../../configstore"; +import { configstore } from "../../configstore"; -import { doSetup } from "../../../init/features/project"; -import * as projectManager from "../../../management/projects"; -import * as prompt from "../../../prompt"; -import { Config } from "../../../config"; +import { doSetup } from "./project"; +import * as projectManager from "../../management/projects"; +import * as prompt from "../../prompt"; +import { Config } from "../../config"; +import { FirebaseProjectMetadata } from "../../types/project"; -const TEST_FIREBASE_PROJECT: projectManager.FirebaseProjectMetadata = { +const TEST_FIREBASE_PROJECT: FirebaseProjectMetadata = { projectId: "my-project-123", projectNumber: "123456789", displayName: "my-project", @@ -115,7 +116,7 @@ describe("project", () => { let err; try { await doSetup(setup, emptyConfig, options); - } catch (e) { + } catch (e: any) { err = e; } @@ -159,7 +160,7 @@ describe("project", () => { let err; try { await doSetup(setup, emptyConfig, options); - } catch (e) { + } catch (e: any) { err = e; } diff --git a/src/init/features/project.ts b/src/init/features/project.ts index e52d848c83d..d1b1e89bc0b 100644 --- a/src/init/features/project.ts +++ b/src/init/features/project.ts @@ -1,16 +1,16 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as _ from "lodash"; import { FirebaseError } from "../../error"; import { addFirebaseToCloudProjectAndLog, createFirebaseProjectAndLog, - FirebaseProjectMetadata, getFirebaseProject, getOrPromptProject, PROJECTS_CREATE_QUESTIONS, promptAvailableProjectId, } from "../../management/projects"; +import { FirebaseProjectMetadata } from "../../types/project"; import { logger } from "../../logger"; import { prompt, promptOnce } from "../../prompt"; import * as utils from "../../utils"; @@ -44,7 +44,7 @@ function toProjectInfo(projectMetaData: FirebaseProjectMetadata): ProjectInfo { async function promptAndCreateNewProject(): Promise { utils.logBullet( "If you want to create a project in a Google Cloud organization or folder, please use " + - `"firebase projects:create" instead, and return to this command when you've created the project.` + `"firebase projects:create" instead, and return to this command when you've created the project.`, ); const promptAnswer: { projectId?: string; displayName?: string } = {}; await prompt(promptAnswer, PROJECTS_CREATE_QUESTIONS); @@ -109,7 +109,7 @@ export async function doSetup(setup: any, config: any, options: any): Promise { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let askWriteProjectFileStub: sinon.SinonStub; + let promptStub: sinon.SinonStub; + + beforeEach(() => { + askWriteProjectFileStub = sandbox.stub(Config.prototype, "askWriteProjectFile"); + promptStub = sandbox.stub(prompt, "promptOnce"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("doSetup", () => { + it("should set up the correct properties in the project", async () => { + const setup = { + config: {}, + rcfile: {}, + projectId: "my-project-123", + projectLocation: "us-central", + }; + promptStub.returns("storage.rules"); + askWriteProjectFileStub.resolves(); + + await doSetup(setup, new Config("/path/to/src", {})); + + expect(_.get(setup, "config.storage.rules")).to.deep.equal("storage.rules"); + }); + + it("should error when cloud resource location is not set", async () => { + const setup = { + config: {}, + rcfile: {}, + projectId: "my-project-123", + }; + + await expect(doSetup(setup, new Config("/path/to/src", {}))).to.eventually.be.rejectedWith( + FirebaseError, + "Cloud resource location is not set", + ); + }); + }); +}); diff --git a/src/init/features/storage.ts b/src/init/features/storage.ts index 0db674109c5..b8d011729d7 100644 --- a/src/init/features/storage.ts +++ b/src/init/features/storage.ts @@ -1,14 +1,11 @@ -import * as clc from "cli-color"; -import * as fs from "fs"; +import * as clc from "colorette"; import { logger } from "../../logger"; import { promptOnce } from "../../prompt"; import { ensureLocationSet } from "../../ensureCloudResourceLocation"; +import { readTemplateSync } from "../../templates"; -const RULES_TEMPLATE = fs.readFileSync( - __dirname + "/../../../templates/init/storage/storage.rules", - "utf8" -); +const RULES_TEMPLATE = readTemplateSync("init/storage/storage.rules"); export async function doSetup(setup: any, config: any): Promise { setup.config.storage = {}; diff --git a/src/init/index.ts b/src/init/index.ts index 08ae3a9a329..df1ac749976 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -1,43 +1,58 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import { capitalize } from "lodash"; +import * as clc from "colorette"; + +import { FirebaseError } from "../error"; import { logger } from "../logger"; -import * as _features from "./features"; -import * as utils from "../utils"; +import * as features from "./features"; +import { RCData } from "../rc"; -export interface Indexable { - [key: string]: T; -} -export interface RCFile { - projects: Indexable; -} export interface Setup { - config: Indexable; - rcfile: RCFile; + config: Record; + rcfile: RCData; features?: string[]; featureArg?: boolean; - project?: Indexable; + project?: Record; projectId?: string; projectLocation?: string; } -// TODO: Convert features/index.js to TypeScript so it exports -// as an indexable type instead of doing this cast. -const features = _features as Indexable; +const featureFns = new Map Promise>([ + ["account", features.account], + ["database", features.database], + ["firestore", features.firestore], + ["dataconnect", features.dataconnect], + ["dataconnect:sdk", features.dataconnectSdk], + ["functions", features.functions], + ["hosting", features.hosting], + ["storage", features.storage], + ["emulators", features.emulators], + ["extensions", features.extensions], + ["project", features.project], // always runs, sets up .firebaserc + ["remoteconfig", features.remoteconfig], + ["hosting:github", features.hostingGithub], + ["genkit", features.genkit], +]); export async function init(setup: Setup, config: any, options: any): Promise { - const nextFeature = setup.features ? setup.features.shift() : undefined; + const nextFeature = setup.features?.shift(); if (nextFeature) { - if (!features[nextFeature]) { - return utils.reject( - clc.bold(nextFeature) + - " is not a valid feature. Must be one of " + - _.without(_.keys(features), "project").join(", ") + if (!featureFns.has(nextFeature)) { + const availableFeatures = Object.keys(features) + .filter((f) => f !== "project") + .join(", "); + throw new FirebaseError( + `${clc.bold(nextFeature)} is not a valid feature. Must be one of ${availableFeatures}`, ); } - logger.info(clc.bold("\n" + clc.white("=== ") + _.capitalize(nextFeature) + " Setup")); + logger.info(clc.bold(`\n${clc.white("===")} ${capitalize(nextFeature)} Setup`)); - await Promise.resolve(features[nextFeature](setup, config, options)); + const fn = featureFns.get(nextFeature); + if (!fn) { + // We've already checked that the function exists, so this really should never happen. + throw new FirebaseError(`We've lost the function to init ${nextFeature}`, { exit: 2 }); + } + await fn(setup, config, options); return init(setup, config, options); } } diff --git a/src/init/spawn.ts b/src/init/spawn.ts new file mode 100644 index 00000000000..d44b0b60efe --- /dev/null +++ b/src/init/spawn.ts @@ -0,0 +1,22 @@ +import * as spawn from "cross-spawn"; +import { logger } from "../logger"; + +export function wrapSpawn(cmd: string, args: string[], projectDir: string): Promise { + return new Promise((resolve, reject) => { + const installer = spawn(cmd, args, { + cwd: projectDir, + stdio: "inherit", + }); + + installer.on("error", (err: any) => { + logger.debug(err.stack); + }); + + installer.on("close", (code) => { + if (code === 0) { + return resolve(); + } + return reject(); + }); + }); +} diff --git a/src/listFiles.spec.ts b/src/listFiles.spec.ts new file mode 100644 index 00000000000..db7d3f2ec1e --- /dev/null +++ b/src/listFiles.spec.ts @@ -0,0 +1,33 @@ +import { expect } from "chai"; + +import { listFiles } from "./listFiles"; +import { FIXTURE_DIR } from "./test/fixtures/ignores"; + +describe("listFiles", () => { + // for details, see the file structure and firebase.json in test/fixtures/ignores + it("should ignore firebase-debug.log, specified ignores, and nothing else", () => { + const fileNames = listFiles(FIXTURE_DIR, [ + "index.ts", + "**/.*", + "firebase.json", + "ignored.txt", + "ignored/**/*.txt", + ]); + expect(fileNames).to.have.members(["index.html", "ignored/index.html", "present/index.html"]); + }); + + it("should allow us to not specify additional ignores", () => { + const fileNames = listFiles(FIXTURE_DIR); + expect(fileNames.sort()).to.have.members([ + ".hiddenfile", + "index.ts", + "firebase.json", + "ignored.txt", + "ignored/deeper/index.txt", + "ignored/ignore.txt", + "ignored/index.html", + "index.html", + "present/index.html", + ]); + }); +}); diff --git a/src/listFiles.ts b/src/listFiles.ts index 495b05a7ca3..731694f5302 100644 --- a/src/listFiles.ts +++ b/src/listFiles.ts @@ -7,6 +7,6 @@ export function listFiles(cwd: string, ignore: string[] = []): string[] { follow: true, ignore: ["**/firebase-debug.log", "**/firebase-debug.*.log", ".firebase/*"].concat(ignore), nodir: true, - nosort: true, + posix: true, }); } diff --git a/src/loadCJSON.js b/src/loadCJSON.js deleted file mode 100644 index 8b8e114ac17..00000000000 --- a/src/loadCJSON.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; - -var { FirebaseError } = require("./error"); -var cjson = require("cjson"); - -module.exports = function (path) { - try { - return cjson.load(path); - } catch (e) { - if (e.code === "ENOENT") { - throw new FirebaseError("File " + path + " does not exist", { exit: 1 }); - } - throw new FirebaseError("Parse Error in " + path + ":\n\n" + e.message); - } -}; diff --git a/src/loadCJSON.ts b/src/loadCJSON.ts new file mode 100644 index 00000000000..bf419f10d36 --- /dev/null +++ b/src/loadCJSON.ts @@ -0,0 +1,16 @@ +import { FirebaseError } from "./error"; +import * as cjson from "cjson"; + +/** + * Loads CJSON from given path. + */ +export function loadCJSON(path: string): any { + try { + return cjson.load(path); + } catch (e: any) { + if (e.code === "ENOENT") { + throw new FirebaseError(`File ${path} does not exist`); + } + throw new FirebaseError(`Parse Error in ${path}:\n\n${e.message}`); + } +} diff --git a/src/localFunction.js b/src/localFunction.js deleted file mode 100644 index f59ff0050aa..00000000000 --- a/src/localFunction.js +++ /dev/null @@ -1,221 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var request = require("request"); - -var { encodeFirestoreValue } = require("./firestore/encodeFirestoreValue"); -var utils = require("./utils"); - -/** - * @constructor - * @this LocalFunction - * - * @param {object} trigger - * @param {object=} urls - * @param {object=} controller - */ -var LocalFunction = function (trigger, urls, controller) { - const isCallable = _.get(trigger, ["labels", "deployment-callable"], "false"); - - this.id = trigger.id; - this.name = trigger.name; - this.eventTrigger = trigger.eventTrigger; - this.httpsTrigger = trigger.httpsTrigger; - this.controller = controller; - this.url = _.get(urls, this.id); - - if (this.httpsTrigger) { - if (isCallable == "true") { - this.call = this._constructCallableFunc.bind(this); - } else { - this.call = request.defaults({ - callback: this._requestCallBack, - baseUrl: this.url, - uri: "", - }); - } - } else { - this.call = this._call.bind(this); - } -}; - -LocalFunction.prototype._isDatabaseFunc = function (eventTrigger) { - return utils.getFunctionsEventProvider(eventTrigger.eventType) === "Database"; -}; - -LocalFunction.prototype._isFirestoreFunc = function (eventTrigger) { - return utils.getFunctionsEventProvider(eventTrigger.eventType) === "Firestore"; -}; - -LocalFunction.prototype._substituteParams = function (resource, params) { - var wildcardRegex = new RegExp("{[^/{}]*}", "g"); - return resource.replace(wildcardRegex, function (wildcard) { - var wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard - var sub = _.get(params, wildcardNoBraces); - return sub || wildcardNoBraces + _.random(1, 9); - }); -}; - -LocalFunction.prototype._constructCallableFunc = function (data, opts) { - opts = opts || {}; - - var headers = {}; - if (_.has(opts, "instanceIdToken")) { - headers["Firebase-Instance-ID-Token"] = opts.instanceIdToken; - } - - return request.post({ - callback: this._requestCallBack, - baseUrl: this.url, - uri: "", - body: { data: data }, - json: true, - headers: headers, - }); -}; - -LocalFunction.prototype._constructAuth = function (auth, authType) { - if (_.get(auth, "admin") || _.get(auth, "variable")) { - return auth; // User is providing the wire auth format already. - } - if (typeof authType !== "undefined") { - switch (authType) { - case "USER": - return { - variable: { - uid: _.get(auth, "uid", ""), - token: _.get(auth, "token", {}), - }, - }; - case "ADMIN": - if (_.get(auth, "uid") || _.get(auth, "token")) { - throw new Error("authType and auth are incompatible."); - } - return { admin: true }; - case "UNAUTHENTICATED": - if (_.get(auth, "uid") || _.get(auth, "token")) { - throw new Error("authType and auth are incompatible."); - } - return { admin: false }; - default: - throw new Error( - "Unrecognized authType, valid values are: " + "ADMIN, USER, and UNAUTHENTICATED" - ); - } - } - if (auth) { - return { - variable: { - uid: auth.uid, - token: auth.token || {}, - }, - }; - } - // Default to admin - return { admin: true }; -}; - -LocalFunction.prototype._makeFirestoreValue = function (input) { - if (typeof input === "undefined" || _.isEmpty(input)) { - // Document does not exist. - return {}; - } - if (typeof input !== "object") { - throw new Error("Firestore data must be key-value pairs."); - } - var currentTime = new Date().toISOString(); - return { - fields: encodeFirestoreValue(input), - createTime: currentTime, - updateTime: currentTime, - }; -}; - -LocalFunction.prototype._requestCallBack = function (err, response, body) { - if (err) { - return console.warn("\nERROR SENDING REQUEST: " + err); - } - var status = response ? response.statusCode + ", " : ""; - - // If the body is a string we want to check if we can parse it as JSON - // and pretty-print it. We can't blindly stringify because stringifying - // a string results in some ugly escaping. - var bodyString = body; - if (typeof body === "string") { - try { - bodyString = JSON.stringify(JSON.parse(bodyString), null, 2); - } catch (e) { - // Ignore - } - } else { - bodyString = JSON.stringify(body, null, 2); - } - - return console.log("\nRESPONSE RECEIVED FROM FUNCTION: " + status + bodyString); -}; - -LocalFunction.prototype._call = function (data, opts) { - opts = opts || {}; - var operationType; - var dataPayload; - - if (this.httpsTrigger) { - this.controller.call(this.name, data || {}); - } else if (this.eventTrigger) { - if (this._isDatabaseFunc(this.eventTrigger)) { - operationType = _.last(this.eventTrigger.eventType.split(".")); - switch (operationType) { - case "create": - dataPayload = { - data: null, - delta: data, - }; - break; - case "delete": - dataPayload = { - data: data, - delta: null, - }; - break; - default: - // 'update' or 'write' - dataPayload = { - data: data.before, - delta: data.after, - }; - } - opts.resource = this._substituteParams(this.eventTrigger.resource, opts.params); - opts.auth = this._constructAuth(opts.auth, opts.authType); - this.controller.call(this.name, dataPayload, opts); - } else if (this._isFirestoreFunc(this.eventTrigger)) { - operationType = _.last(this.eventTrigger.eventType.split(".")); - switch (operationType) { - case "create": - dataPayload = { - value: this._makeFirestoreValue(data), - oldValue: {}, - }; - break; - case "delete": - dataPayload = { - value: {}, - oldValue: this._makeFirestoreValue(data), - }; - break; - default: - // 'update' or 'write' - dataPayload = { - value: this._makeFirestoreValue(data.after), - oldValue: this._makeFirestoreValue(data.before), - }; - } - opts.resource = this._substituteParams(this.eventTrigger.resource, opts.params); - this.controller.call(this.name, dataPayload, opts); - } else { - this.controller.call(this.name, data || {}, opts); - } - } - return "Successfully invoked function."; -}; - -module.exports = LocalFunction; diff --git a/src/localFunction.spec.ts b/src/localFunction.spec.ts new file mode 100644 index 00000000000..fc21f37c248 --- /dev/null +++ b/src/localFunction.spec.ts @@ -0,0 +1,73 @@ +import { expect } from "chai"; + +import LocalFunction from "./localFunction"; +import { EmulatedTriggerDefinition } from "./emulator/functionsEmulatorShared"; +import { FunctionsEmulatorShell } from "./emulator/functionsEmulatorShell"; + +const EMULATED_TRIGGER: EmulatedTriggerDefinition = { + id: "fn", + region: "us-central1", + platform: "gcfv1", + availableMemoryMb: 1024, + entryPoint: "test-resource", + name: "test-resource", + timeoutSeconds: 3, +}; + +describe("constructAuth", () => { + const lf = new LocalFunction(EMULATED_TRIGGER, {}, {} as FunctionsEmulatorShell); + + describe("#_constructAuth", () => { + it("warn if opts.auth and opts.authType are conflicting", () => { + expect(() => { + return lf.constructAuth({ uid: "something" }, "UNAUTHENTICATED"); + }).to.throw("incompatible"); + + expect(() => { + return lf.constructAuth({ admin: false, uid: "something" }, "ADMIN"); + }).to.throw("incompatible"); + }); + + it("construct the correct auth for admin users", () => { + expect(lf.constructAuth(undefined, "ADMIN")).to.deep.equal({ admin: true }); + }); + + it("construct the correct auth for unauthenticated users", () => { + expect(lf.constructAuth(undefined, "UNAUTHENTICATED")).to.deep.equal({ + admin: false, + }); + }); + + it("construct the correct auth for authenticated users", () => { + expect(lf.constructAuth(undefined, "USER")).to.deep.equal({ + admin: false, + variable: { uid: "", token: {} }, + }); + expect(lf.constructAuth({ uid: "11" }, "USER")).to.deep.equal({ + admin: false, + variable: { uid: "11", token: {} }, + }); + }); + + it("leaves auth untouched if it already follows wire format", () => { + const auth = { admin: false, variable: { uid: "something" } }; + expect(lf.constructAuth(auth)).to.deep.equal(auth); + }); + }); +}); + +describe("makeFirestoreValue", () => { + const lf = new LocalFunction(EMULATED_TRIGGER, {}, {} as FunctionsEmulatorShell); + + it("returns {} when there is no data", () => { + expect(lf.makeFirestoreValue()).to.deep.equal({}); + expect(lf.makeFirestoreValue(null)).to.deep.equal({}); + expect(lf.makeFirestoreValue({})).to.deep.equal({}); + }); + + it("throws error when data is not key-value pairs", () => { + expect(() => { + return lf.makeFirestoreValue("string"); + }).to.throw(Error); + }); +}); diff --git a/src/localFunction.ts b/src/localFunction.ts new file mode 100644 index 00000000000..e85595bc09f --- /dev/null +++ b/src/localFunction.ts @@ -0,0 +1,382 @@ +import * as uuid from "uuid"; + +import { encodeFirestoreValue } from "./firestore/encodeFirestoreValue"; +import * as utils from "./utils"; +import { EmulatedTriggerDefinition } from "./emulator/functionsEmulatorShared"; +import { FunctionsEmulatorShell } from "./emulator/functionsEmulatorShell"; +import { AuthMode, AuthType, EventOptions } from "./emulator/events/types"; +import { Client, ClientResponse, ClientVerbOptions } from "./apiv2"; + +// HTTPS_SENTINEL is sent when a HTTPS call is made via functions:shell. +export const HTTPS_SENTINEL = "Request sent to function."; + +/** + * LocalFunction produces EmulatedTriggerDefinition into a function that can be called inside the nodejs repl. + */ +export default class LocalFunction { + private url?: string; + private paramWildcardRegex = new RegExp("{[^/{}]*}", "g"); + + constructor( + private trigger: EmulatedTriggerDefinition, + urls: Record, + private controller: FunctionsEmulatorShell, + ) { + this.url = urls[trigger.id]; + } + + private substituteParams(resource: string, params?: Record): string { + if (!params) { + return resource; + } + return resource.replace(this.paramWildcardRegex, (wildcard: string) => { + const wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard + const sub = params?.[wildcardNoBraces]; + return sub || wildcardNoBraces + utils.randomInt(1, 9); + }); + } + + private constructCallableFunc(data: string | object, opts: { instanceIdToken?: string }): void { + opts = opts || {}; + + const headers: Record = {}; + if (opts.instanceIdToken) { + headers["Firebase-Instance-ID-Token"] = opts.instanceIdToken; + } + + if (!this.url) { + throw new Error("No URL provided"); + } + + const client = new Client({ urlPrefix: this.url, auth: false }); + void client + .post("", data, { headers }) + .then((res) => { + this.requestCallBack(undefined, res, res.body); + }) + .catch((err) => { + this.requestCallBack(err); + }); + } + + private constructHttpsFunc(): requestShim { + if (!this.url) { + throw new Error("No URL provided"); + } + const callClient = new Client({ urlPrefix: this.url, auth: false }); + type verbFn = (...args: any) => Promise; + const verbFactory = ( + hasRequestBody: boolean, + method: ( + path: string, + bodyOrOpts?: any, + opts?: ClientVerbOptions, + ) => Promise>, + ): verbFn => { + return async (pathOrOptions?: string | HttpsOptions, options?: HttpsOptions) => { + const { path, opts } = this.extractArgs(pathOrOptions, options); + try { + const res = hasRequestBody + ? await method(path, opts.body, toClientVerbOptions(opts)) + : await method(path, toClientVerbOptions(opts)); + this.requestCallBack(undefined, res, res.body); + } catch (err) { + this.requestCallBack(err); + } + return HTTPS_SENTINEL; + }; + }; + + const shim = verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => { + const req = Object.assign(opts || {}, { + path: path, + body: json, + method: opts?.method || "GET", + }); + return callClient.request(req); + }); + const verbs: verbMethods = { + post: verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => + callClient.post(path, json, opts), + ), + put: verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => + callClient.put(path, json, opts), + ), + patch: verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => + callClient.patch(path, json, opts), + ), + get: verbFactory(false, (path: string, opts?: ClientVerbOptions) => + callClient.get(path, opts), + ), + del: verbFactory(false, (path: string, opts?: ClientVerbOptions) => + callClient.delete(path, opts), + ), + delete: verbFactory(false, (path: string, opts?: ClientVerbOptions) => + callClient.delete(path, opts), + ), + options: verbFactory(false, (path: string, opts?: ClientVerbOptions) => + callClient.options(path, opts), + ), + }; + return Object.assign(shim, verbs); + } + + private extractArgs( + pathOrOptions?: string | HttpsOptions, + options?: HttpsOptions, + ): { path: string; opts: HttpsOptions } { + // Case: No arguments provided + if (!pathOrOptions && !options) { + return { path: "/", opts: {} }; + } + + // Case: pathOrOptions is provided as a string + if (typeof pathOrOptions === "string") { + return { path: pathOrOptions, opts: options || {} }; + } + + // Case: pathOrOptions is an object (HttpsOptions), and options is not provided + if (typeof pathOrOptions !== "string" && !!pathOrOptions && !options) { + return { path: "/", opts: pathOrOptions }; + } + + // Error case: Invalid combination of arguments + if (typeof pathOrOptions !== "string" || !options) { + throw new Error( + `Invalid argument combination: Expected a string and/or HttpsOptions, got ${typeof pathOrOptions} and ${typeof options}`, + ); + } + + // Default return, though this point should not be reached + return { path: "/", opts: {} }; + } + + constructAuth(auth?: EventOptions["auth"], authType?: AuthType): AuthMode { + if (auth?.admin || auth?.variable) { + return { + admin: auth.admin || false, + variable: auth.variable, + }; // User is providing the wire auth format already. + } + if (authType) { + switch (authType) { + case "USER": + return { + admin: false, + variable: { + uid: auth?.uid ?? "", + token: auth?.token ?? {}, + }, + }; + case "ADMIN": + if (auth?.uid || auth?.token) { + throw new Error("authType and auth are incompatible."); + } + return { admin: true }; + case "UNAUTHENTICATED": + if (auth?.uid || auth?.token) { + throw new Error("authType and auth are incompatible."); + } + return { admin: false }; + default: + throw new Error( + "Unrecognized authType, valid values are: " + "ADMIN, USER, and UNAUTHENTICATED", + ); + } + } + if (auth) { + return { + admin: false, + variable: { + uid: auth.uid ?? "", + token: auth.token || {}, + }, + }; + } + // Default to admin + return { admin: true }; + } + + makeFirestoreValue(input?: unknown) { + if ( + typeof input === "undefined" || + input === null || + (typeof input === "object" && Object.keys(input).length === 0) + ) { + // Document does not exist. + return {}; + } + if (typeof input !== "object") { + throw new Error("Firestore data must be key-value pairs."); + } + const currentTime = new Date().toISOString(); + return { + fields: encodeFirestoreValue(input), + createTime: currentTime, + updateTime: currentTime, + }; + } + + private requestCallBack(err: unknown, response?: ClientResponse, body?: string | object) { + if (err) { + return console.warn("\nERROR SENDING REQUEST: " + err); + } + const status = response ? response.status + ", " : ""; + + // If the body is a string we want to check if we can parse it as JSON + // and pretty-print it. We can't blindly stringify because stringifying + // a string results in some ugly escaping. + let bodyString = body; + if (typeof bodyString === "string") { + try { + bodyString = JSON.stringify(JSON.parse(bodyString), null, 2); + } catch (e) { + // Ignore + } + } else { + bodyString = JSON.stringify(body, null, 2); + } + + return console.log("\nRESPONSE RECEIVED FROM FUNCTION: " + status + bodyString); + } + + private isDatabaseFn(eventTrigger: Required["eventTrigger"]) { + return utils.getFunctionsEventProvider(eventTrigger.eventType) === "Database"; + } + private isFirestoreFunc(eventTrigger: Required["eventTrigger"]) { + return utils.getFunctionsEventProvider(eventTrigger.eventType) === "Firestore"; + } + + private isPubsubFunc(eventTrigger: Required["eventTrigger"]) { + return utils.getFunctionsEventProvider(eventTrigger.eventType) === "PubSub"; + } + + private triggerEvent(data: unknown, opts?: EventOptions) { + opts = opts || {}; + let operationType; + let dataPayload; + + if (this.trigger.httpsTrigger) { + this.controller.call(this.trigger, data || {}, opts); + } else if (this.trigger.eventTrigger) { + if (this.isDatabaseFn(this.trigger.eventTrigger)) { + operationType = utils.last(this.trigger.eventTrigger.eventType.split(".")); + switch (operationType) { + case "create": + case "created": + dataPayload = { + data: null, + delta: data, + }; + break; + case "delete": + case "deleted": + dataPayload = { + data: data, + delta: null, + }; + break; + default: + // 'update', 'updated', 'write', or 'written' + dataPayload = { + data: (data as any).before, + delta: (data as any).after, + }; + } + const resource = + this.trigger.eventTrigger.resource ?? + this.trigger.eventTrigger.eventFilterPathPatterns?.ref; + opts.resource = this.substituteParams(resource!, opts.params); + opts.auth = this.constructAuth(opts.auth, opts.authType); + this.controller.call(this.trigger, dataPayload, opts); + } else if (this.isFirestoreFunc(this.trigger.eventTrigger)) { + operationType = utils.last(this.trigger.eventTrigger.eventType.split(".")); + switch (operationType) { + case "create": + case "created": + dataPayload = { + value: this.makeFirestoreValue(data), + oldValue: {}, + }; + break; + case "delete": + case "deleted": + dataPayload = { + value: {}, + oldValue: this.makeFirestoreValue(data), + }; + break; + default: + // 'update', 'updated', 'write' or 'written' + dataPayload = { + value: this.makeFirestoreValue((data as any).after), + oldValue: this.makeFirestoreValue((data as any).before), + }; + } + const resource = + this.trigger.eventTrigger.resource ?? + this.trigger.eventTrigger.eventFilterPathPatterns?.document; + opts.resource = this.substituteParams(resource!, opts.params); + this.controller.call(this.trigger, dataPayload, opts); + } else if (this.isPubsubFunc(this.trigger.eventTrigger)) { + dataPayload = data; + if (this.trigger.platform === "gcfv2") { + dataPayload = { message: { ...(data as any), messageId: uuid.v4() } }; + } + this.controller.call(this.trigger, dataPayload || {}, opts); + } else { + this.controller.call(this.trigger, data || {}, opts); + } + } + return "Successfully invoked function."; + } + + makeFn() { + if (this.trigger.httpsTrigger) { + const isCallable = !!this.trigger.labels?.["deployment-callable"]; + if (isCallable) { + return (data: any, opt: any) => this.constructCallableFunc(data, opt); + } else { + return this.constructHttpsFunc(); + } + } else { + return (data: any, opt: any) => this.triggerEvent(data, opt); + } + } +} + +// requestShim is a minimal implementation of the public API of the deprecated `request` package +// We expose it as part of `functions:shell` so that we can keep the previous API while removing +// our dependency on `request`. +interface requestShim extends verbMethods { + (...args: any): any; + // TODO(taeold/blidd/joehan) What other methods do we need to add? form? json? multipart? +} + +interface verbMethods { + get(...args: any): any; + post(...args: any): any; + put(...args: any): any; + patch(...args: any): any; + del(...args: any): any; + delete(...args: any): any; + options(...args: any): any; +} + +// HttpsOptions is a subset of request's CoreOptions +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/request/index.d.ts#L107 +// We intentionally omit options that are likely useless for `functions:shell` +type HttpsOptions = { + method?: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD"; + headers?: Record; + body?: any; + qs?: any; +}; + +function toClientVerbOptions(opts: HttpsOptions): ClientVerbOptions { + return { + method: opts.method, + headers: opts.headers, + queryParams: opts.qs, + }; +} diff --git a/src/logError.js b/src/logError.js deleted file mode 100644 index 9cf538dd299..00000000000 --- a/src/logError.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; - -const { logger } = require("./logger"); -var clc = require("cli-color"); - -/* istanbul ignore next */ -module.exports = function (error) { - if (error.children && error.children.length) { - logger.error(clc.bold.red("Error:"), clc.underline(error.message) + ":"); - error.children.forEach(function (child) { - var out = "- "; - if (child.name) { - out += clc.bold(child.name) + " "; - } - out += child.message; - - logger.error(out); - }); - } else { - if (error.original) { - logger.debug(error.original.stack); - } - logger.error(); - logger.error(clc.bold.red("Error:"), error.message); - } - if (error.context) { - logger.debug("Error Context:", JSON.stringify(error.context, undefined, 2)); - } -}; diff --git a/src/logError.ts b/src/logError.ts new file mode 100644 index 00000000000..9703ffcd952 --- /dev/null +++ b/src/logError.ts @@ -0,0 +1,27 @@ +import { logger } from "./logger"; +import * as clc from "colorette"; + +/* istanbul ignore next */ +export function logError(error: any): void { + if (error.children && error.children.length) { + logger.error(clc.bold(clc.red("Error:")), clc.underline(error.message) + ":"); + error.children.forEach((child: any) => { + let out = "- "; + if (child.name) { + out += clc.bold(child.name) + " "; + } + out += child.message; + + logger.error(out); + }); + } else { + if (error.original) { + logger.debug(error.original.stack); + } + logger.error(); + logger.error(clc.bold(clc.red("Error:")), error.message); + } + if (error.context) { + logger.debug("Error Context:", JSON.stringify(error.context, undefined, 2)); + } +} diff --git a/src/logger.ts b/src/logger.ts index 96ef299e4b4..dc411690d78 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,6 +1,21 @@ import * as winston from "winston"; import * as Transport from "winston-transport"; +import { isVSCodeExtension } from "./utils"; +import { EventEmitter } from "events"; + +/** vsceLogEmitter passes CLI logs along to VSCode. + Events are of the format winston.LogEntry + Example usage: + + vsceLogEmitter.on("log", (logEntry) => { + if (logEntry.level == "error") { + console.log(logEntry.message) + } + }) +*/ +export const vsceLogEmitter = new EventEmitter(); + export type LogLevel = | "error" | "warn" @@ -80,6 +95,27 @@ function annotateDebugLines(logger: winston.Logger): winston.Logger { return logger; } +function maybeUseVSCodeLogger(logger: winston.Logger): winston.Logger { + if (!isVSCodeExtension()) { + return logger; + } + const oldLogFunc = logger.log.bind(logger); + const vsceLogger: winston.LogMethod = function ( + levelOrEntry: string | winston.LogEntry, + message?: string | Error, + ...meta: any[] + ): winston.Logger { + if (message) { + vsceLogEmitter.emit("log", { level: levelOrEntry, message }); + } else { + vsceLogEmitter.emit("log", levelOrEntry); + } + return oldLogFunc(levelOrEntry as string, message as string, ...meta); + }; + logger.log = vsceLogger; + return logger; +} + const rawLogger = winston.createLogger(); // Set a default silent logger to suppress logs during tests rawLogger.add(new winston.transports.Console({ silent: true })); @@ -91,4 +127,6 @@ rawLogger.exitOnError = false; // allow error parameters. // Casting looks super dodgy, but it should be safe because we know the underlying code // handles all parameter types we care about. -export const logger = (annotateDebugLines(expandErrors(rawLogger)) as unknown) as Logger; +export const logger = maybeUseVSCodeLogger( + annotateDebugLines(expandErrors(rawLogger)), +) as unknown as Logger; diff --git a/src/management/apps.spec.ts b/src/management/apps.spec.ts new file mode 100644 index 00000000000..de9c7553507 --- /dev/null +++ b/src/management/apps.spec.ts @@ -0,0 +1,666 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as fs from "fs"; +import * as nock from "nock"; + +import * as api from "../api"; +import { + AndroidAppMetadata, + AppPlatform, + APP_LIST_PAGE_SIZE, + createAndroidApp, + createIosApp, + createWebApp, + getAppConfig, + getAppConfigFile, + getAppPlatform, + IosAppMetadata, + listFirebaseApps, + WebAppMetadata, +} from "./apps"; +import * as pollUtils from "../operation-poller"; +import { FirebaseError } from "../error"; +import { firebaseApiOrigin } from "../api"; + +const PROJECT_ID = "the-best-firebase-project"; +const OPERATION_RESOURCE_NAME_1 = "operations/cp.11111111111111111"; +const APP_ID = "appId"; +const IOS_APP_BUNDLE_ID = "bundleId"; +const IOS_APP_STORE_ID = "appStoreId"; +const IOS_APP_DISPLAY_NAME = "iOS app"; +const ANDROID_APP_PACKAGE_NAME = "com.google.packageName"; +const ANDROID_APP_DISPLAY_NAME = "Android app"; +const WEB_APP_DISPLAY_NAME = "Web app"; + +function generateIosAppList(counts: number): IosAppMetadata[] { + return Array.from(Array(counts), (_, i: number) => ({ + name: `projects/project-id-${i}/apps/app-id-${i}`, + projectId: `project-id`, + appId: `app-id-${i}`, + platform: AppPlatform.IOS, + displayName: `Project ${i}`, + bundleId: `bundle-id-${i}`, + })); +} + +function generateAndroidAppList(counts: number): AndroidAppMetadata[] { + return Array.from(Array(counts), (_, i: number) => ({ + name: `projects/project-id-${i}/apps/app-id-${i}`, + projectId: `project-id`, + appId: `app-id-${i}`, + platform: AppPlatform.ANDROID, + displayName: `Project ${i}`, + packageName: `package.name.app${i}`, + })); +} + +function generateWebAppList(counts: number): WebAppMetadata[] { + return Array.from(Array(counts), (_, i: number) => ({ + name: `projects/project-id-${i}/apps/app-id-${i}`, + projectId: `project-id`, + appId: `app-id-${i}`, + platform: AppPlatform.WEB, + displayName: `Project ${i}`, + })); +} + +describe("App management", () => { + let sandbox: sinon.SinonSandbox; + let pollOperationStub: sinon.SinonStub; + let readFileSyncStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + pollOperationStub = sandbox.stub(pollUtils, "pollOperation").throws("Unexpected poll call"); + readFileSyncStub = sandbox.stub(fs, "readFileSync").throws("Unxpected readFileSync call"); + nock.disableNetConnect(); + }); + + afterEach(() => { + sandbox.restore(); + nock.enableNetConnect(); + }); + + describe("getAppPlatform", () => { + it("should return the iOS platform", () => { + expect(getAppPlatform("IOS")).to.equal(AppPlatform.IOS); + expect(getAppPlatform("iOS")).to.equal(AppPlatform.IOS); + expect(getAppPlatform("Ios")).to.equal(AppPlatform.IOS); + }); + + it("should return the Android platform", () => { + expect(getAppPlatform("Android")).to.equal(AppPlatform.ANDROID); + expect(getAppPlatform("ANDROID")).to.equal(AppPlatform.ANDROID); + expect(getAppPlatform("aNDroiD")).to.equal(AppPlatform.ANDROID); + }); + + it("should return the Web platform", () => { + expect(getAppPlatform("Web")).to.equal(AppPlatform.WEB); + expect(getAppPlatform("WEB")).to.equal(AppPlatform.WEB); + expect(getAppPlatform("wEb")).to.equal(AppPlatform.WEB); + }); + + it("should return the ANY platform", () => { + expect(getAppPlatform("")).to.equal(AppPlatform.ANY); + }); + + it("should throw if the platform is unknown", () => { + expect(() => getAppPlatform("unknown")).to.throw( + FirebaseError, + "Unexpected platform. Only iOS, Android, and Web apps are supported", + ); + }); + }); + + describe("createIosApp", () => { + it("should resolve with app data if it succeeds", async () => { + const expectedAppMetadata = { + appId: APP_ID, + displayName: IOS_APP_DISPLAY_NAME, + bundleId: IOS_APP_BUNDLE_ID, + appStoreId: IOS_APP_STORE_ID, + }; + nock(firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/iosApps`) + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); + pollOperationStub.onFirstCall().resolves(expectedAppMetadata); + + const resultAppInfo = await createIosApp(PROJECT_ID, { + displayName: IOS_APP_DISPLAY_NAME, + bundleId: IOS_APP_BUNDLE_ID, + appStoreId: IOS_APP_STORE_ID, + }); + + expect(resultAppInfo).to.deep.equal(expectedAppMetadata); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.calledOnceWith({ + pollerName: "Create iOS app Poller", + apiOrigin: api.firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: OPERATION_RESOURCE_NAME_1, + }); + }); + + it("should reject if app creation api call fails", async () => { + nock(firebaseApiOrigin()).post(`/v1beta1/projects/${PROJECT_ID}/iosApps`).reply(404); + + let err; + try { + await createIosApp(PROJECT_ID, { + displayName: IOS_APP_DISPLAY_NAME, + bundleId: IOS_APP_BUNDLE_ID, + appStoreId: IOS_APP_STORE_ID, + }); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to create iOS app for project ${PROJECT_ID}. See firebase-debug.log for more info.`, + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.not.called; + }); + + it("should reject if polling throws error", async () => { + const expectedError = new Error("Permission denied"); + nock(firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/iosApps`) + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); + pollOperationStub.onFirstCall().rejects(expectedError); + + let err; + try { + await createIosApp(PROJECT_ID, { + displayName: IOS_APP_DISPLAY_NAME, + bundleId: IOS_APP_BUNDLE_ID, + appStoreId: IOS_APP_STORE_ID, + }); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to create iOS app for project ${PROJECT_ID}. See firebase-debug.log for more info.`, + ); + expect(err.original).to.equal(expectedError); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.calledOnceWith({ + pollerName: "Create iOS app Poller", + apiOrigin: api.firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: OPERATION_RESOURCE_NAME_1, + }); + }); + }); + + describe("createAndroidApp", () => { + it("should resolve with app data if it succeeds", async () => { + const expectedAppMetadata = { + appId: APP_ID, + displayName: ANDROID_APP_DISPLAY_NAME, + packageName: ANDROID_APP_PACKAGE_NAME, + }; + nock(firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/androidApps`) + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); + pollOperationStub.onFirstCall().resolves(expectedAppMetadata); + + const resultAppInfo = await createAndroidApp(PROJECT_ID, { + displayName: ANDROID_APP_DISPLAY_NAME, + packageName: ANDROID_APP_PACKAGE_NAME, + }); + + expect(resultAppInfo).to.equal(expectedAppMetadata); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.calledOnceWith({ + pollerName: "Create Android app Poller", + apiOrigin: api.firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: OPERATION_RESOURCE_NAME_1, + }); + }); + + it("should reject if app creation api call fails", async () => { + nock(firebaseApiOrigin()).post(`/v1beta1/projects/${PROJECT_ID}/androidApps`).reply(404); + + let err; + try { + await createAndroidApp(PROJECT_ID, { + displayName: ANDROID_APP_DISPLAY_NAME, + packageName: ANDROID_APP_PACKAGE_NAME, + }); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to create Android app for project ${PROJECT_ID}. See firebase-debug.log for more info.`, + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.not.called; + }); + + it("should reject if polling throws error", async () => { + const expectedError = new Error("Permission denied"); + nock(firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/androidApps`) + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); + pollOperationStub.onFirstCall().rejects(expectedError); + + let err; + try { + await createAndroidApp(PROJECT_ID, { + displayName: ANDROID_APP_DISPLAY_NAME, + packageName: ANDROID_APP_PACKAGE_NAME, + }); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to create Android app for project ${PROJECT_ID}. See firebase-debug.log for more info.`, + ); + expect(err.original).to.equal(expectedError); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.calledOnceWith({ + pollerName: "Create Android app Poller", + apiOrigin: api.firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: OPERATION_RESOURCE_NAME_1, + }); + }); + }); + + describe("createWebApp", () => { + it("should resolve with app data if it succeeds", async () => { + const expectedAppMetadata = { + appId: APP_ID, + displayName: WEB_APP_DISPLAY_NAME, + }; + nock(firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/webApps`) + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); + pollOperationStub.onFirstCall().resolves(expectedAppMetadata); + + const resultAppInfo = await createWebApp(PROJECT_ID, { displayName: WEB_APP_DISPLAY_NAME }); + + expect(resultAppInfo).to.equal(expectedAppMetadata); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.calledOnceWith({ + pollerName: "Create Web app Poller", + apiOrigin: api.firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: OPERATION_RESOURCE_NAME_1, + }); + }); + + it("should reject if app creation api call fails", async () => { + nock(firebaseApiOrigin()).post(`/v1beta1/projects/${PROJECT_ID}/webApps`).reply(404); + + let err; + try { + await createWebApp(PROJECT_ID, { displayName: WEB_APP_DISPLAY_NAME }); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to create Web app for project ${PROJECT_ID}. See firebase-debug.log for more info.`, + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.not.called; + }); + + it("should reject if polling throws error", async () => { + const expectedError = new Error("Permission denied"); + nock(firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}/webApps`) + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); + pollOperationStub.onFirstCall().rejects(expectedError); + + let err; + try { + await createWebApp(PROJECT_ID, { displayName: WEB_APP_DISPLAY_NAME }); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to create Web app for project ${PROJECT_ID}. See firebase-debug.log for more info.`, + ); + expect(err.original).to.equal(expectedError); + expect(nock.isDone()).to.be.true; + expect(pollOperationStub).to.be.calledOnceWith({ + pollerName: "Create Web app Poller", + apiOrigin: api.firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: OPERATION_RESOURCE_NAME_1, + }); + }); + }); + + describe("listFirebaseApps", () => { + it("should resolve with app list if it succeeds with only 1 api call", async () => { + const appCountsPerPlatform = 3; + const expectedAppList = [ + ...generateIosAppList(appCountsPerPlatform), + ...generateAndroidAppList(appCountsPerPlatform), + ...generateWebAppList(appCountsPerPlatform), + ]; + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}:searchApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(200, { apps: expectedAppList }); + + const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.ANY); + + expect(apps).to.deep.equal(expectedAppList); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with iOS app list", async () => { + const appCounts = 10; + const expectedAppList = generateIosAppList(appCounts); + const apiResponseAppList = expectedAppList.map((app) => { + // TODO: this is gross typing to make it invalid. Might be possible to do better. + const iosApp: any = { ...app }; + delete iosApp.platform; + return iosApp; + }); + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/iosApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(200, { apps: apiResponseAppList }); + + const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.IOS); + + expect(apps).to.deep.equal(expectedAppList); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with Android app list", async () => { + const appCounts = 10; + const expectedAppList = generateAndroidAppList(appCounts); + const apiResponseAppList = expectedAppList.map((app) => { + // TODO: this is gross typing to make it invalid. Might be possible to do better. + const androidApps: any = { ...app }; + delete androidApps.platform; + return androidApps; + }); + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/androidApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(200, { apps: apiResponseAppList }); + + const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.ANDROID); + + expect(apps).to.deep.equal(expectedAppList); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with Web app list", async () => { + const appCounts = 10; + const expectedAppList = generateWebAppList(appCounts); + const apiResponseAppList = expectedAppList.map((app) => { + // TODO: this is gross typing to make it invalid. Might be possible to do better. + const webApp: any = { ...app }; + delete webApp.platform; + return webApp; + }); + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/webApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(200, { apps: apiResponseAppList }); + + const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.WEB); + + expect(apps).to.deep.equal(expectedAppList); + expect(nock.isDone()).to.be.true; + }); + + it("should concatenate pages to get app list if it succeeds", async () => { + const appCountsPerPlatform = 3; + const pageSize = 5; + const nextPageToken = "next-page-token"; + const expectedAppList = [ + ...generateIosAppList(appCountsPerPlatform), + ...generateAndroidAppList(appCountsPerPlatform), + ...generateWebAppList(appCountsPerPlatform), + ]; + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}:searchApps`) + .query({ pageSize }) + .reply(200, { apps: expectedAppList.slice(0, pageSize), nextPageToken }); + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}:searchApps`) + .query({ pageSize, pageToken: nextPageToken }) + .reply(200, { apps: expectedAppList.slice(pageSize, appCountsPerPlatform * 3) }); + + const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.ANY, pageSize); + + expect(apps).to.deep.equal(expectedAppList); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the first api call fails", async () => { + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}:searchApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(404); + + let err; + try { + await listFirebaseApps(PROJECT_ID, AppPlatform.ANY); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + "Failed to list Firebase apps. See firebase-debug.log for more info.", + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + + it("should rejects if error is thrown in subsequence api call", async () => { + const appCounts = 10; + const pageSize = 5; + const nextPageToken = "next-page-token"; + const expectedAppList = generateAndroidAppList(appCounts); + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}:searchApps`) + .query({ pageSize }) + .reply(200, { apps: expectedAppList.slice(0, pageSize), nextPageToken }); + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}:searchApps`) + .query({ pageSize, pageToken: nextPageToken }) + .reply(404); + + let err; + try { + await listFirebaseApps(PROJECT_ID, AppPlatform.ANY, pageSize); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + "Failed to list Firebase apps. See firebase-debug.log for more info.", + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the list iOS apps fails", async () => { + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/iosApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(404); + + let err; + try { + await listFirebaseApps(PROJECT_ID, AppPlatform.IOS); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + "Failed to list Firebase IOS apps. See firebase-debug.log for more info.", + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the list Android apps fails", async () => { + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/androidApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(404); + + let err; + try { + await listFirebaseApps(PROJECT_ID, AppPlatform.ANDROID); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + "Failed to list Firebase ANDROID apps. See firebase-debug.log for more info.", + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the list Web apps fails", async () => { + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${PROJECT_ID}/webApps`) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(404); + + let err; + try { + await listFirebaseApps(PROJECT_ID, AppPlatform.WEB); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + "Failed to list Firebase WEB apps. See firebase-debug.log for more info.", + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getAppConfigFile", () => { + it("should resolve with iOS app configuration if it succeeds", async () => { + const expectedConfigFileContent = "test iOS configuration"; + const mockBase64Content = Buffer.from(expectedConfigFileContent).toString("base64"); + nock(firebaseApiOrigin()).get(`/v1beta1/projects/-/iosApps/${APP_ID}/config`).reply(200, { + configFilename: "GoogleService-Info.plist", + configFileContents: mockBase64Content, + }); + + const configData = await getAppConfig(APP_ID, AppPlatform.IOS); + const fileData = getAppConfigFile(configData, AppPlatform.IOS); + + expect(fileData).to.deep.equal({ + fileName: "GoogleService-Info.plist", + fileContents: expectedConfigFileContent, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with Web app configuration if it succeeds", async () => { + const mockWebConfig = { + projectId: PROJECT_ID, + appId: APP_ID, + apiKey: "api-key", + }; + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/-/webApps/${APP_ID}/config`) + .reply(200, mockWebConfig); + readFileSyncStub.onFirstCall().returns("{/*--CONFIG--*/}"); + + const configData = await getAppConfig(APP_ID, AppPlatform.WEB); + const fileData = getAppConfigFile(configData, AppPlatform.WEB); + + expect(fileData).to.deep.equal({ + fileName: "google-config.js", + fileContents: JSON.stringify(mockWebConfig, null, 2), + }); + expect(nock.isDone()).to.be.true; + expect(readFileSyncStub).to.be.calledOnce; + }); + }); + + describe("getAppConfig", () => { + it("should resolve with iOS app configuration if it succeeds", async () => { + const mockBase64Content = Buffer.from("test iOS configuration").toString("base64"); + nock(firebaseApiOrigin()).get(`/v1beta1/projects/-/iosApps/${APP_ID}/config`).reply(200, { + configFilename: "GoogleService-Info.plist", + configFileContents: mockBase64Content, + }); + + const configData = await getAppConfig(APP_ID, AppPlatform.IOS); + + expect(configData).to.deep.equal({ + configFilename: "GoogleService-Info.plist", + configFileContents: mockBase64Content, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with Android app configuration if it succeeds", async () => { + const mockBase64Content = Buffer.from("test Android configuration").toString("base64"); + nock(firebaseApiOrigin()).get(`/v1beta1/projects/-/androidApps/${APP_ID}/config`).reply(200, { + configFilename: "google-services.json", + configFileContents: mockBase64Content, + }); + + const configData = await getAppConfig(APP_ID, AppPlatform.ANDROID); + + expect(configData).to.deep.equal({ + configFilename: "google-services.json", + configFileContents: mockBase64Content, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with Web app configuration if it succeeds", async () => { + const mockWebConfig = { + projectId: PROJECT_ID, + appId: APP_ID, + apiKey: "api-key", + }; + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/-/webApps/${APP_ID}/config`) + .reply(200, mockWebConfig); + + const configData = await getAppConfig(APP_ID, AppPlatform.WEB); + + expect(configData).to.deep.equal(mockWebConfig); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if api request fails", async () => { + nock(firebaseApiOrigin()).get(`/v1beta1/projects/-/androidApps/${APP_ID}/config`).reply(404); + + let err; + try { + await getAppConfig(APP_ID, AppPlatform.ANDROID); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + "Failed to get ANDROID app configuration. See firebase-debug.log for more info.", + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/management/apps.ts b/src/management/apps.ts index 5c43f98cea8..1a9e7b75b83 100644 --- a/src/management/apps.ts +++ b/src/management/apps.ts @@ -1,12 +1,12 @@ -import * as fs from "fs"; - -import * as api from "../api"; +import { Client } from "../apiv2"; +import { firebaseApiOrigin } from "../api"; import { FirebaseError } from "../error"; import { logger } from "../logger"; import { pollOperation } from "../operation-poller"; +import { readTemplateSync } from "../templates"; const TIMEOUT_MILLIS = 30000; -const APP_LIST_PAGE_SIZE = 100; +export const APP_LIST_PAGE_SIZE = 100; const CREATE_APP_API_REQUEST_TIMEOUT_MILLIS = 15000; const WEB_CONFIG_FILE_NAME = "google-config.js"; @@ -84,6 +84,8 @@ export function getAppPlatform(platform: string): AppPlatform { } } +const apiClient = new Client({ urlPrefix: firebaseApiOrigin(), apiVersion: "v1beta1" }); + /** * Send an API request to create a new Firebase iOS app and poll the LRO to get the new app * information. @@ -93,28 +95,31 @@ export function getAppPlatform(platform: string): AppPlatform { */ export async function createIosApp( projectId: string, - options: { displayName?: string; appStoreId?: string; bundleId: string } + options: { displayName?: string; appStoreId?: string; bundleId: string }, ): Promise { try { - const response = await api.request("POST", `/v1beta1/projects/${projectId}/iosApps`, { - auth: true, - origin: api.firebaseApiOrigin, + const response = await apiClient.request< + { displayName?: string; appStoreId?: string; bundleId: string }, + { name: string } + >({ + method: "POST", + path: `/projects/${projectId}/iosApps`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, - data: options, + body: options, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const appData = await pollOperation({ pollerName: "Create iOS app Poller", - apiOrigin: api.firebaseApiOrigin, + apiOrigin: firebaseApiOrigin(), apiVersion: "v1beta1", operationResourceName: response.body.name /* LRO resource name */, }); return appData; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to create iOS app for project ${projectId}. See firebase-debug.log for more info.`, - { exit: 2, original: err } + { exit: 2, original: err }, ); } } @@ -128,31 +133,34 @@ export async function createIosApp( */ export async function createAndroidApp( projectId: string, - options: { displayName?: string; packageName: string } + options: { displayName?: string; packageName: string }, ): Promise { try { - const response = await api.request("POST", `/v1beta1/projects/${projectId}/androidApps`, { - auth: true, - origin: api.firebaseApiOrigin, + const response = await apiClient.request< + { displayName?: string; packageName: string }, + { name: string } + >({ + method: "POST", + path: `/projects/${projectId}/androidApps`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, - data: options, + body: options, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const appData = await pollOperation({ pollerName: "Create Android app Poller", - apiOrigin: api.firebaseApiOrigin, + apiOrigin: firebaseApiOrigin(), apiVersion: "v1beta1", operationResourceName: response.body.name /* LRO resource name */, }); return appData; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to create Android app for project ${projectId}. See firebase-debug.log for more info.`, { exit: 2, original: err, - } + }, ); } } @@ -166,28 +174,28 @@ export async function createAndroidApp( */ export async function createWebApp( projectId: string, - options: { displayName?: string } + options: { displayName?: string }, ): Promise { try { - const response = await api.request("POST", `/v1beta1/projects/${projectId}/webApps`, { - auth: true, - origin: api.firebaseApiOrigin, + const response = await apiClient.request<{ displayName?: string }, { name: string }>({ + method: "POST", + path: `/projects/${projectId}/webApps`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, - data: options, + body: options, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const appData = await pollOperation({ pollerName: "Create Web app Poller", - apiOrigin: api.firebaseApiOrigin, + apiOrigin: firebaseApiOrigin(), apiVersion: "v1beta1", operationResourceName: response.body.name /* LRO resource name */, }); return appData; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to create Web app for project ${projectId}. See firebase-debug.log for more info.`, - { exit: 2, original: err } + { exit: 2, original: err }, ); } } @@ -211,7 +219,7 @@ function getListAppsResourceString(projectId: string, platform: AppPlatform): st throw new FirebaseError("Unexpected platform. Only support iOS, Android and Web apps"); } - return `/v1beta1/projects/${projectId}${resourceSuffix}`; + return `/projects/${projectId}${resourceSuffix}`; } /** @@ -225,28 +233,27 @@ function getListAppsResourceString(projectId: string, platform: AppPlatform): st export async function listFirebaseApps( projectId: string, platform: AppPlatform, - pageSize: number = APP_LIST_PAGE_SIZE + pageSize: number = APP_LIST_PAGE_SIZE, ): Promise { const apps: AppMetadata[] = []; try { - let nextPageToken = ""; + let nextPageToken: string | undefined; do { - const pageTokenQueryString = nextPageToken ? `&pageToken=${nextPageToken}` : ""; - const response = await api.request( - "GET", - getListAppsResourceString(projectId, platform) + - `?pageSize=${pageSize}${pageTokenQueryString}`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: TIMEOUT_MILLIS, - } - ); + const queryParams: { pageSize: number; pageToken?: string } = { pageSize }; + if (nextPageToken) { + queryParams.pageToken = nextPageToken; + } + const response = await apiClient.request({ + method: "GET", + path: getListAppsResourceString(projectId, platform), + queryParams, + timeout: TIMEOUT_MILLIS, + }); if (response.body.apps) { const appsOnPage = response.body.apps.map( // app.platform does not exist if we use the endpoint for a specific platform // eslint-disable-next-line @typescript-eslint/no-explicit-any - (app: any) => (app.platform ? app : { ...app, platform }) + (app: any) => (app.platform ? app : { ...app, platform }), ); apps.push(...appsOnPage); } @@ -254,7 +261,7 @@ export async function listFirebaseApps( } while (nextPageToken); return apps; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to list Firebase ${platform === AppPlatform.ANY ? "" : platform + " "}` + @@ -262,7 +269,7 @@ export async function listFirebaseApps( { exit: 2, original: err, - } + }, ); } } @@ -283,13 +290,13 @@ function getAppConfigResourceString(appId: string, platform: AppPlatform): strin throw new FirebaseError("Unexpected app platform"); } - return `/v1beta1/projects/-/${platformResource}/${appId}/config`; + return `/projects/-/${platformResource}/${appId}/config`; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function parseConfigFromResponse(responseBody: any, platform: AppPlatform): AppConfigurationData { if (platform === AppPlatform.WEB) { - const JS_TEMPLATE = fs.readFileSync(__dirname + "/../../templates/setup/web.js", "utf8"); + const JS_TEMPLATE = readTemplateSync("setup/web.js"); return { fileName: WEB_CONFIG_FILE_NAME, fileContents: JS_TEMPLATE.replace("{/*--CONFIG--*/}", JSON.stringify(responseBody, null, 2)), @@ -323,24 +330,23 @@ export function getAppConfigFile(config: any, platform: AppPlatform): AppConfigu */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function getAppConfig(appId: string, platform: AppPlatform): Promise { - let response; try { - response = await api.request("GET", getAppConfigResourceString(appId, platform), { - auth: true, - origin: api.firebaseApiOrigin, + const response = await apiClient.request({ + method: "GET", + path: getAppConfigResourceString(appId, platform), timeout: TIMEOUT_MILLIS, }); - } catch (err) { + return response.body; + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to get ${platform} app configuration. See firebase-debug.log for more info.`, { exit: 2, original: err, - } + }, ); } - return response.body; } /** @@ -351,24 +357,21 @@ export async function getAppConfig(appId: string, platform: AppPlatform): Promis */ export async function listAppAndroidSha( projectId: string, - appId: string + appId: string, ): Promise { const shaCertificates: AppAndroidShaData[] = []; try { - const response = await api.request( - "GET", - `/v1beta1/projects/${projectId}/androidApps/${appId}/sha`, - { - auth: true, - origin: api.firebaseApiOrigin, - } - ); + const response = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/androidApps/${appId}/sha`, + timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, + }); if (response.body.certificates) { shaCertificates.push(...response.body.certificates); } return shaCertificates; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to list SHA certificate hashes for Android app ${appId}.` + @@ -376,7 +379,7 @@ export async function listAppAndroidSha( { exit: 2, original: err, - } + }, ); } } @@ -391,31 +394,25 @@ export async function listAppAndroidSha( export async function createAppAndroidSha( projectId: string, appId: string, - options: { shaHash: string; certType: string } + options: { shaHash: string; certType: string }, ): Promise { try { - const response = await api.request( - "POST", - `/v1beta1/projects/${projectId}/androidApps/${appId}/sha`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, - data: options, - } - ); - + const response = await apiClient.request<{ shaHash: string; certType: string }, any>({ + method: "POST", + path: `/projects/${projectId}/androidApps/${appId}/sha`, + body: options, + timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, + }); const shaCertificate = response.body; - return shaCertificate; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to create SHA certificate hash for Android app ${appId}. See firebase-debug.log for more info.`, { exit: 2, original: err, - } + }, ); } } @@ -429,27 +426,22 @@ export async function createAppAndroidSha( export async function deleteAppAndroidSha( projectId: string, appId: string, - shaId: string + shaId: string, ): Promise { try { - await api.request( - "DELETE", - `/v1beta1/projects/${projectId}/androidApps/${appId}/sha/${shaId}`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, - data: null, - } - ); - } catch (err) { + await apiClient.request({ + method: "DELETE", + path: `/projects/${projectId}/androidApps/${appId}/sha/${shaId}`, + timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, + }); + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to delete SHA certificate hash for Android app ${appId}. See firebase-debug.log for more info.`, { exit: 2, original: err, - } + }, ); } } diff --git a/src/management/database.spec.ts b/src/management/database.spec.ts new file mode 100644 index 00000000000..a37ea6c32d1 --- /dev/null +++ b/src/management/database.spec.ts @@ -0,0 +1,430 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as nock from "nock"; + +import * as api from "../api"; + +import { + DatabaseLocation, + DatabaseInstance, + DatabaseInstanceType, + DatabaseInstanceState, + getDatabaseInstanceDetails, + createInstance, + listDatabaseInstances, + checkInstanceNameAvailable, + MGMT_API_VERSION, + APP_LIST_PAGE_SIZE, +} from "./database"; +import { FirebaseError } from "../error"; + +const PROJECT_ID = "the-best-firebase-project"; +const DATABASE_INSTANCE_NAME = "some_instance"; +const SOME_DATABASE_INSTANCE: DatabaseInstance = { + name: DATABASE_INSTANCE_NAME, + location: DatabaseLocation.US_CENTRAL1, + project: PROJECT_ID, + databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.US_CENTRAL1), + type: DatabaseInstanceType.USER_DATABASE, + state: DatabaseInstanceState.ACTIVE, +}; + +const SOME_DATABASE_INSTANCE_EUROPE_WEST1: DatabaseInstance = { + name: DATABASE_INSTANCE_NAME, + location: DatabaseLocation.EUROPE_WEST1, + project: PROJECT_ID, + databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.EUROPE_WEST1), + type: DatabaseInstanceType.USER_DATABASE, + state: DatabaseInstanceState.ACTIVE, +}; + +const INSTANCE_RESPONSE_US_CENTRAL1 = { + name: `projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances/${DATABASE_INSTANCE_NAME}`, + project: PROJECT_ID, + databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.US_CENTRAL1), + type: DatabaseInstanceType.USER_DATABASE, + state: DatabaseInstanceState.ACTIVE, +}; + +const INSTANCE_RESPONSE_EUROPE_WEST1 = { + name: `projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances/${DATABASE_INSTANCE_NAME}`, + project: PROJECT_ID, + databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.EUROPE_WEST1), + type: DatabaseInstanceType.USER_DATABASE, + state: DatabaseInstanceState.ACTIVE, +}; + +function generateDatabaseUrl(instanceName: string, location: DatabaseLocation): string { + if (location === DatabaseLocation.ANY) { + throw new Error("can't generate url for any location"); + } + if (location === DatabaseLocation.US_CENTRAL1) { + return `https://${instanceName}.firebaseio.com`; + } + return `https://${instanceName}.${location}.firebasedatabase.app`; +} + +function generateInstanceList(counts: number, location: DatabaseLocation): DatabaseInstance[] { + return Array.from(Array(counts), (_, i: number) => { + const name = `my-db-instance-${i}`; + return { + name: name, + location: location, + project: PROJECT_ID, + databaseUrl: generateDatabaseUrl(name, location), + type: DatabaseInstanceType.USER_DATABASE, + state: DatabaseInstanceState.ACTIVE, + }; + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function generateInstanceListApiResponse(counts: number, location: DatabaseLocation): any[] { + return Array.from(Array(counts), (_, i: number) => { + const name = `my-db-instance-${i}`; + return { + name: `projects/${PROJECT_ID}/locations/${location}/instances/${name}`, + project: PROJECT_ID, + databaseUrl: generateDatabaseUrl(name, location), + type: DatabaseInstanceType.USER_DATABASE, + state: DatabaseInstanceState.ACTIVE, + }; + }); +} + +describe("Database management", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + nock.disableNetConnect(); + }); + + afterEach(() => { + nock.enableNetConnect(); + sandbox.restore(); + }); + + describe("getDatabaseInstanceDetails", () => { + it("should resolve with DatabaseInstance if API call succeeds", async () => { + const expectedDatabaseInstance = SOME_DATABASE_INSTANCE; + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/-/instances/${DATABASE_INSTANCE_NAME}`, + ) + .reply(200, INSTANCE_RESPONSE_US_CENTRAL1); + + const resultDatabaseInstance = await getDatabaseInstanceDetails( + PROJECT_ID, + DATABASE_INSTANCE_NAME, + ); + + expect(resultDatabaseInstance).to.deep.equal(expectedDatabaseInstance); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if API call fails", async () => { + const badInstanceName = "non-existent-instance"; + nock(api.rtdbManagementOrigin()) + .get(`/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/-/instances/${badInstanceName}`) + .reply(404); + let err; + try { + await getDatabaseInstanceDetails(PROJECT_ID, badInstanceName); + } catch (e: any) { + err = e; + } + expect(err.message).to.equal( + `Failed to get instance details for instance: ${badInstanceName}. See firebase-debug.log for more details.`, + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createInstance", () => { + it("should resolve with new DatabaseInstance if API call succeeds", async () => { + const expectedDatabaseInstance = SOME_DATABASE_INSTANCE_EUROPE_WEST1; + nock(api.rtdbManagementOrigin()) + .post( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances`, + ) + .query({ databaseId: DATABASE_INSTANCE_NAME }) + .reply(200, INSTANCE_RESPONSE_EUROPE_WEST1); + const resultDatabaseInstance = await createInstance( + PROJECT_ID, + DATABASE_INSTANCE_NAME, + DatabaseLocation.EUROPE_WEST1, + DatabaseInstanceType.USER_DATABASE, + ); + expect(resultDatabaseInstance).to.deep.equal(expectedDatabaseInstance); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if API call fails", async () => { + const badInstanceName = "non-existent-instance"; + nock(api.rtdbManagementOrigin()) + .post( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances`, + ) + .query({ databaseId: badInstanceName }) + .reply(404); + + let err; + try { + await createInstance( + PROJECT_ID, + badInstanceName, + DatabaseLocation.US_CENTRAL1, + DatabaseInstanceType.DEFAULT_DATABASE, + ); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to create instance: ${badInstanceName}. See firebase-debug.log for more details.`, + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("checkInstanceNameAvailable", () => { + it("should resolve with new DatabaseInstance if specified instance name is available and API call succeeds", async () => { + nock(api.rtdbManagementOrigin()) + .post( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances`, + ) + .query({ databaseId: DATABASE_INSTANCE_NAME, validateOnly: true }) + .reply(200, INSTANCE_RESPONSE_EUROPE_WEST1); + + const output = await checkInstanceNameAvailable( + PROJECT_ID, + DATABASE_INSTANCE_NAME, + DatabaseInstanceType.USER_DATABASE, + DatabaseLocation.EUROPE_WEST1, + ); + + expect(output).to.deep.equal({ available: true }); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with suggested instance names if the API call fails with suggestions ", async () => { + const badInstanceName = "invalid:database|name"; + const expectedErrorObj = { + error: { + details: [ + { + metadata: { + suggested_database_ids: "dbName1,dbName2,dbName3", + }, + }, + ], + }, + }; + nock(api.rtdbManagementOrigin()) + .post( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances`, + ) + .query({ databaseId: badInstanceName, validateOnly: true }) + .reply(409, expectedErrorObj); + + const output = await checkInstanceNameAvailable( + PROJECT_ID, + badInstanceName, + DatabaseInstanceType.USER_DATABASE, + DatabaseLocation.EUROPE_WEST1, + ); + + expect(output).to.deep.equal({ + available: false, + suggestedIds: ["dbName1", "dbName2", "dbName3"], + }); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if API call fails without suggestions", async () => { + const badInstanceName = "non-existent-instance"; + const expectedErrorObj = { + error: { + details: [ + { + metadata: {}, + }, + ], + }, + }; + nock(api.rtdbManagementOrigin()) + .post( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances`, + ) + .query({ databaseId: badInstanceName, validateOnly: true }) + .reply(409, expectedErrorObj); + + let err; + try { + await checkInstanceNameAvailable( + PROJECT_ID, + badInstanceName, + DatabaseInstanceType.DEFAULT_DATABASE, + DatabaseLocation.US_CENTRAL1, + ); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to validate Realtime Database instance name: ${badInstanceName}.`, + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "409"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listDatabaseInstances", () => { + it("should resolve with instance list if it succeeds with only 1 api call", async () => { + const pageSize = 5; + const instancesPerLocation = 2; + const expectedInstanceList = [ + ...generateInstanceList(instancesPerLocation, DatabaseLocation.US_CENTRAL1), + ...generateInstanceList(instancesPerLocation, DatabaseLocation.EUROPE_WEST1), + ]; + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.ANY}/instances`, + ) + .query({ pageSize }) + .reply(200, { + instances: [ + ...generateInstanceListApiResponse(instancesPerLocation, DatabaseLocation.US_CENTRAL1), + ...generateInstanceListApiResponse(instancesPerLocation, DatabaseLocation.EUROPE_WEST1), + ], + }); + + const instances = await listDatabaseInstances(PROJECT_ID, DatabaseLocation.ANY, pageSize); + + expect(instances).to.deep.equal(expectedInstanceList); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with specific location", async () => { + const instancesPerLocation = 2; + const expectedInstancesList = generateInstanceList( + instancesPerLocation, + DatabaseLocation.US_CENTRAL1, + ); + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances`, + ) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(200, { + instances: [ + ...generateInstanceListApiResponse(instancesPerLocation, DatabaseLocation.US_CENTRAL1), + ], + }); + + const instances = await listDatabaseInstances(PROJECT_ID, DatabaseLocation.US_CENTRAL1); + + expect(instances).to.deep.equal(expectedInstancesList); + expect(nock.isDone()).to.be.true; + }); + + it("should concatenate pages to get instances list if it succeeds", async () => { + const countPerLocation = 3; + const pageSize = 5; + const nextPageToken = "next-page-token"; + const expectedInstancesList = [ + ...generateInstanceList(countPerLocation, DatabaseLocation.US_CENTRAL1), + ...generateInstanceList(countPerLocation, DatabaseLocation.EUROPE_WEST1), + ...generateInstanceList(countPerLocation, DatabaseLocation.EUROPE_WEST1), + ]; + const expectedResponsesList = [ + ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.US_CENTRAL1), + ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.EUROPE_WEST1), + ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.EUROPE_WEST1), + ]; + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.ANY}/instances`, + ) + .query({ pageSize: pageSize }) + .reply(200, { + instances: expectedResponsesList.slice(0, pageSize), + nextPageToken, + }); + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.ANY}/instances`, + ) + .query({ pageSize: pageSize, pageToken: nextPageToken }) + .reply(200, { + instances: expectedResponsesList.slice(pageSize), + }); + + const instances = await listDatabaseInstances(PROJECT_ID, DatabaseLocation.ANY, pageSize); + + expect(instances).to.deep.equal(expectedInstancesList); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the first api call fails", async () => { + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.ANY}/instances`, + ) + .query({ pageSize: APP_LIST_PAGE_SIZE }) + .reply(404); + + let err; + try { + await listDatabaseInstances(PROJECT_ID, DatabaseLocation.ANY); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + "Failed to list Firebase Realtime Database instances. See firebase-debug.log for more info.", + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if error is thrown in subsequent api call", async () => { + const countPerLocation = 5; + const pageSize = 5; + const nextPageToken = "next-page-token"; + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances`, + ) + .query({ pageSize: pageSize }) + .reply(200, { + instances: [ + ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.US_CENTRAL1), + ].slice(0, pageSize), + nextPageToken, + }); + nock(api.rtdbManagementOrigin()) + .get( + `/${MGMT_API_VERSION}/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances`, + ) + .query({ pageSize: pageSize, pageToken: nextPageToken }) + .reply(404); + + let err; + try { + await listDatabaseInstances(PROJECT_ID, DatabaseLocation.US_CENTRAL1, pageSize); + } catch (e: any) { + err = e; + } + + expect(err.message).to.equal( + `Failed to list Firebase Realtime Database instances for location ${DatabaseLocation.US_CENTRAL1}. See firebase-debug.log for more info.`, + ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/management/database.ts b/src/management/database.ts index c96b3007457..e9e28ff6c25 100644 --- a/src/management/database.ts +++ b/src/management/database.ts @@ -3,14 +3,16 @@ * Internal documentation: https://source.corp.google.com/piper///depot/google3/google/firebase/database/v1beta/rtdb_service.proto */ -import * as api from "../api"; +import { Client } from "../apiv2"; +import { Constants } from "../emulator/constants"; +import { FirebaseError } from "../error"; import { logger } from "../logger"; +import { rtdbManagementOrigin } from "../api"; import * as utils from "../utils"; -import { FirebaseError } from "../error"; -import { Constants } from "../emulator/constants"; -const MGMT_API_VERSION = "v1beta"; + +export const MGMT_API_VERSION = "v1beta"; +export const APP_LIST_PAGE_SIZE = 100; const TIMEOUT_MILLIS = 10000; -const APP_LIST_PAGE_SIZE = 100; const INSTANCE_RESOURCE_NAME_REGEX = /projects\/([^/]+?)\/locations\/([^/]+?)\/instances\/([^/]*)/; export enum DatabaseInstanceType { @@ -42,6 +44,8 @@ export interface DatabaseInstance { state: DatabaseInstanceState; } +const apiClient = new Client({ urlPrefix: rtdbManagementOrigin(), apiVersion: MGMT_API_VERSION }); + /** * Populate instanceDetails in commandOptions. * @param options command options that will be modified to add instanceDetails. @@ -59,21 +63,16 @@ export async function populateInstanceDetails(options: any): Promise { */ export async function getDatabaseInstanceDetails( projectId: string, - instanceName: string + instanceName: string, ): Promise { try { - const response = await api.request( - "GET", - `/${MGMT_API_VERSION}/projects/${projectId}/locations/-/instances/${instanceName}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: TIMEOUT_MILLIS, - } - ); - + const response = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/locations/-/instances/${instanceName}`, + timeout: TIMEOUT_MILLIS, + }); return convertDatabaseInstance(response.body); - } catch (err) { + } catch (err: any) { logger.debug(err.message); const emulatorHost = process.env[Constants.FIREBASE_DATABASE_EMULATOR_HOST]; if (emulatorHost) { @@ -88,12 +87,12 @@ export async function getDatabaseInstanceDetails( state: DatabaseInstanceState.ACTIVE, }); } - return utils.reject( + throw new FirebaseError( `Failed to get instance details for instance: ${instanceName}. See firebase-debug.log for more details.`, { - code: 2, + exit: 2, original: err, - } + }, ); } } @@ -109,31 +108,26 @@ export async function createInstance( projectId: string, instanceName: string, location: DatabaseLocation, - databaseType: DatabaseInstanceType + databaseType: DatabaseInstanceType, ): Promise { try { - const response = await api.request( - "POST", - `/${MGMT_API_VERSION}/projects/${projectId}/locations/${location}/instances?databaseId=${instanceName}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: TIMEOUT_MILLIS, - data: { - type: databaseType, - }, - } - ); + const response = await apiClient.request({ + method: "POST", + path: `/projects/${projectId}/locations/${location}/instances`, + queryParams: { databaseId: instanceName }, + body: { type: databaseType }, + timeout: TIMEOUT_MILLIS, + }); return convertDatabaseInstance(response.body); - } catch (err) { + } catch (err: any) { logger.debug(err.message); return utils.reject( `Failed to create instance: ${instanceName}. See firebase-debug.log for more details.`, { code: 2, original: err, - } + }, ); } } @@ -150,32 +144,25 @@ export async function checkInstanceNameAvailable( projectId: string, instanceName: string, databaseType: DatabaseInstanceType, - location?: DatabaseLocation + location?: DatabaseLocation, ): Promise<{ available: boolean; suggestedIds?: string[] }> { if (!location) { location = DatabaseLocation.US_CENTRAL1; } try { - await api.request( - "POST", - `/${MGMT_API_VERSION}/projects/${projectId}/locations/${location}/instances?databaseId=${instanceName}&validateOnly=true`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: TIMEOUT_MILLIS, - data: { - type: databaseType, - }, - } - ); - return { - available: true, - }; - } catch (err) { + await apiClient.request({ + method: "POST", + path: `/projects/${projectId}/locations/${location}/instances`, + queryParams: { databaseId: instanceName, validateOnly: "true" }, + body: { type: databaseType }, + timeout: TIMEOUT_MILLIS, + }); + return { available: true }; + } catch (err: any) { logger.debug( `Invalid Realtime Database instance name: ${instanceName}.${ err.message ? " " + err.message : "" - }` + }`, ); const errBody = err.context.body.error; if (errBody?.details?.[0]?.metadata?.suggested_database_ids) { @@ -188,7 +175,7 @@ export async function checkInstanceNameAvailable( `Failed to validate Realtime Database instance name: ${instanceName}.`, { original: err, - } + }, ); } } @@ -201,7 +188,7 @@ export async function checkInstanceNameAvailable( */ export function parseDatabaseLocation( location: string, - defaultLocation: DatabaseLocation + defaultLocation: DatabaseLocation, ): DatabaseLocation { if (!location) { return defaultLocation; @@ -217,7 +204,7 @@ export function parseDatabaseLocation( return defaultLocation; default: throw new FirebaseError( - `Unexpected location value: ${location}. Only us-central1, europe-west1, and asia-southeast1 locations are supported` + `Unexpected location value: ${location}. Only us-central1, europe-west1, and asia-southeast1 locations are supported`, ); } } @@ -233,22 +220,22 @@ export function parseDatabaseLocation( export async function listDatabaseInstances( projectId: string, location: DatabaseLocation, - pageSize: number = APP_LIST_PAGE_SIZE + pageSize: number = APP_LIST_PAGE_SIZE, ): Promise { const instances: DatabaseInstance[] = []; try { - let nextPageToken = ""; + let nextPageToken: string | undefined = ""; do { - const pageTokenQueryString = nextPageToken ? `&pageToken=${nextPageToken}` : ""; - const response = await api.request( - "GET", - `/${MGMT_API_VERSION}/projects/${projectId}/locations/${location}/instances?pageSize=${pageSize}${pageTokenQueryString}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: TIMEOUT_MILLIS, - } - ); + const queryParams: { pageSize: number; pageToken?: string } = { pageSize }; + if (nextPageToken) { + queryParams.pageToken = nextPageToken; + } + const response = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/locations/${location}/instances`, + queryParams, + timeout: TIMEOUT_MILLIS, + }); if (response.body.instances) { instances.push(...response.body.instances.map(convertDatabaseInstance)); } @@ -256,7 +243,7 @@ export async function listDatabaseInstances( } while (nextPageToken); return instances; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to list Firebase Realtime Database instances${ @@ -265,7 +252,7 @@ export async function listDatabaseInstances( { exit: 2, original: err, - } + }, ); } } @@ -276,9 +263,9 @@ function convertDatabaseInstance(serverInstance: any): DatabaseInstance { throw new FirebaseError(`DatabaseInstance response is missing field "name"`); } const m = serverInstance.name.match(INSTANCE_RESOURCE_NAME_REGEX); - if (!m || m.length != 4) { + if (!m || m.length !== 4) { throw new FirebaseError( - `Error parsing instance resource name: ${serverInstance.name}, matches: ${m}` + `Error parsing instance resource name: ${serverInstance.name}, matches: ${m}`, ); } return { diff --git a/src/test/management/projects.spec.ts b/src/management/projects.spec.ts similarity index 81% rename from src/test/management/projects.spec.ts rename to src/management/projects.spec.ts index 7280c213ef4..ebe415b4c7b 100644 --- a/src/test/management/projects.spec.ts +++ b/src/management/projects.spec.ts @@ -2,10 +2,12 @@ import { expect } from "chai"; import * as sinon from "sinon"; import * as nock from "nock"; -import * as api from "../../api"; -import * as projectManager from "../../management/projects"; -import * as pollUtils from "../../operation-poller"; -import * as prompt from "../../prompt"; +import * as api from "../api"; +import * as projectManager from "./projects"; +import * as pollUtils from "../operation-poller"; +import * as prompt from "../prompt"; +import { FirebaseError } from "../error"; +import { CloudProjectInfo, FirebaseProjectMetadata } from "../types/project"; const PROJECT_ID = "the-best-firebase-project"; const PROJECT_NUMBER = "1234567890"; @@ -23,7 +25,7 @@ const LOCATION_ID = "location-id"; const PAGE_TOKEN = "page-token"; const NEXT_PAGE_TOKEN = "next-page-token"; -const TEST_FIREBASE_PROJECT: projectManager.FirebaseProjectMetadata = { +const TEST_FIREBASE_PROJECT: FirebaseProjectMetadata = { projectId: "my-project-123", projectNumber: "123456789", displayName: "my-project", @@ -36,7 +38,7 @@ const TEST_FIREBASE_PROJECT: projectManager.FirebaseProjectMetadata = { }, }; -const ANOTHER_FIREBASE_PROJECT: projectManager.FirebaseProjectMetadata = { +const ANOTHER_FIREBASE_PROJECT: FirebaseProjectMetadata = { projectId: "another-project", projectNumber: "987654321", displayName: "another-project", @@ -44,19 +46,19 @@ const ANOTHER_FIREBASE_PROJECT: projectManager.FirebaseProjectMetadata = { resources: {}, }; -const TEST_CLOUD_PROJECT: projectManager.CloudProjectInfo = { +const TEST_CLOUD_PROJECT: CloudProjectInfo = { project: "projects/my-project-123", displayName: "my-project", locationId: "us-central", }; -const ANOTHER_CLOUD_PROJECT: projectManager.CloudProjectInfo = { +const ANOTHER_CLOUD_PROJECT: CloudProjectInfo = { project: "projects/another-project", displayName: "another-project", locationId: "us-central", }; -function generateFirebaseProjectList(counts: number): projectManager.FirebaseProjectMetadata[] { +function generateFirebaseProjectList(counts: number): FirebaseProjectMetadata[] { return Array.from(Array(counts), (_, i: number) => ({ name: `projects/project-id-${i}`, projectId: `project-id-${i}`, @@ -71,7 +73,7 @@ function generateFirebaseProjectList(counts: number): projectManager.FirebasePro })); } -function generateCloudProjectList(counts: number): projectManager.CloudProjectInfo[] { +function generateCloudProjectList(counts: number): CloudProjectInfo[] { return Array.from(Array(counts), (_, i: number) => ({ project: `projects/project-id-${i}`, displayName: `Project ${i}`, @@ -81,17 +83,17 @@ function generateCloudProjectList(counts: number): projectManager.CloudProjectIn describe("Project management", () => { let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; let pollOperationStub: sinon.SinonStub; beforeEach(() => { sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); pollOperationStub = sandbox.stub(pollUtils, "pollOperation").throws("Unexpected poll call"); + nock.disableNetConnect(); }); afterEach(() => { sandbox.restore(); + nock.enableNetConnect(); }); describe("Interactive flows", () => { @@ -104,7 +106,7 @@ describe("Project management", () => { describe("getOrPromptProject", () => { it("should get project from list if it is able to list all projects", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 100 }) .reply(200, { @@ -121,14 +123,14 @@ describe("Project management", () => { }); it("should prompt project id if it is not able to list all projects", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 100 }) .reply(200, { results: [TEST_FIREBASE_PROJECT, ANOTHER_FIREBASE_PROJECT], nextPageToken: "token", }); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects/my-project-123") .reply(200, TEST_FIREBASE_PROJECT); promptOnceStub.resolves("my-project-123"); @@ -142,19 +144,19 @@ describe("Project management", () => { }); it("should throw if there's no project", async () => { - nock(api.firebaseApiOrigin).get("/v1beta1/projects").query({ pageSize: 100 }).reply(200, { + nock(api.firebaseApiOrigin()).get("/v1beta1/projects").query({ pageSize: 100 }).reply(200, { results: [], }); let err; try { await projectManager.getOrPromptProject({}); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "There are no Firebase projects associated with this account." + "There are no Firebase projects associated with this account.", ); expect(promptOnceStub).to.be.not.called; expect(nock.isDone()).to.be.true; @@ -162,7 +164,7 @@ describe("Project management", () => { it("should get the correct project info when --project is supplied", async () => { const options = { project: "my-project-123" }; - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects/my-project-123") .reply(200, TEST_FIREBASE_PROJECT); @@ -175,20 +177,20 @@ describe("Project management", () => { it("should throw error when getFirebaseProject throw an error", async () => { const options = { project: "my-project-123" }; - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects/my-project-123") .reply(500, { error: "Failed to get project" }); let err; try { await projectManager.getOrPromptProject(options); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( "Failed to get Firebase project my-project-123" + - ". Please make sure the project exists and your account has permission to access it." + ". Please make sure the project exists and your account has permission to access it.", ); expect(err.original.toString()).to.contain("Failed to get project"); expect(promptOnceStub).to.be.not.called; @@ -198,7 +200,7 @@ describe("Project management", () => { describe("promptAvailableProjectId", () => { it("should select project from list if it is able to list all projects", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize: 100 }) .reply(200, { @@ -215,7 +217,7 @@ describe("Project management", () => { }); it("should prompt project id if it is not able to list all projects", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize: 100 }) .reply(200, { @@ -233,7 +235,7 @@ describe("Project management", () => { }); it("should throw if there's no project", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize: 100 }) .reply(200, { @@ -243,12 +245,12 @@ describe("Project management", () => { let err; try { await projectManager.promptAvailableProjectId(); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "There are no available Google Cloud projects to add Firebase services." + "There are no available Google Cloud projects to add Firebase services.", ); expect(promptOnceStub).to.be.not.called; expect(nock.isDone()).to.be.true; @@ -264,7 +266,9 @@ describe("Project management", () => { projectId: PROJECT_ID, name: PROJECT_NAME, }; - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_1 } }); + nock(api.resourceManagerOrigin()) + .post("/v1/projects") + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); pollOperationStub.onFirstCall().resolves(expectedProjectInfo); const resultProjectInfo = await projectManager.createCloudProject(PROJECT_ID, { @@ -273,23 +277,17 @@ describe("Project management", () => { }); expect(resultProjectInfo).to.equal(expectedProjectInfo); - expect(apiRequestStub).to.be.calledOnceWith("POST", "/v1/projects", { - auth: true, - origin: api.resourceManagerOrigin, - timeout: 15000, - data: { projectId: PROJECT_ID, name: PROJECT_NAME, parent: PARENT_RESOURCE }, - }); + expect(nock.isDone()).to.be.true; expect(pollOperationStub).to.be.calledOnceWith({ pollerName: "Project Creation Poller", - apiOrigin: api.resourceManagerOrigin, + apiOrigin: api.resourceManagerOrigin(), apiVersion: "v1", operationResourceName: OPERATION_RESOURCE_NAME_1, }); }); it("should reject if Cloud project creation fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); + nock(api.resourceManagerOrigin()).post("/v1/projects").reply(404); let err; try { @@ -297,26 +295,23 @@ describe("Project management", () => { displayName: PROJECT_NAME, parentResource: PARENT_RESOURCE, }); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to create project. See firebase-debug.log for more info." + "Failed to create project. See firebase-debug.log for more info.", ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith("POST", "/v1/projects", { - auth: true, - origin: api.resourceManagerOrigin, - timeout: 15000, - data: { projectId: PROJECT_ID, name: PROJECT_NAME, parent: PARENT_RESOURCE }, - }); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; expect(pollOperationStub).to.be.not.called; }); it("should reject if Cloud project creation polling throws error", async () => { const expectedError = new Error("Entity already exists"); - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_1 } }); + nock(api.resourceManagerOrigin()) + .post("/v1/projects") + .reply(200, { name: OPERATION_RESOURCE_NAME_1 }); pollOperationStub.onFirstCall().rejects(expectedError); let err; @@ -325,23 +320,18 @@ describe("Project management", () => { displayName: PROJECT_NAME, parentResource: PARENT_RESOURCE, }); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to create project. See firebase-debug.log for more info." + "Failed to create project. See firebase-debug.log for more info.", ); expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith("POST", "/v1/projects", { - auth: true, - origin: api.resourceManagerOrigin, - timeout: 15000, - data: { projectId: PROJECT_ID, name: PROJECT_NAME, parent: PARENT_RESOURCE }, - }); + expect(nock.isDone()).to.be.true; expect(pollOperationStub).to.be.calledOnceWith({ pollerName: "Project Creation Poller", - apiOrigin: api.resourceManagerOrigin, + apiOrigin: api.resourceManagerOrigin(), apiVersion: "v1", operationResourceName: OPERATION_RESOURCE_NAME_1, }); @@ -351,7 +341,9 @@ describe("Project management", () => { describe("addFirebaseToCloudProject", () => { it("should resolve with Firebase project data if it succeeds", async () => { const expectFirebaseProjectInfo = { projectId: PROJECT_ID, displayName: PROJECT_NAME }; - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_2 } }); + nock(api.firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}:addFirebase`) + .reply(200, { name: OPERATION_RESOURCE_NAME_2 }); pollOperationStub .onFirstCall() .resolves({ projectId: PROJECT_ID, displayName: PROJECT_NAME }); @@ -359,78 +351,57 @@ describe("Project management", () => { const resultProjectInfo = await projectManager.addFirebaseToCloudProject(PROJECT_ID); expect(resultProjectInfo).to.deep.equal(expectFirebaseProjectInfo); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}:addFirebase`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - } - ); + expect(nock.isDone()).to.be.true; expect(pollOperationStub).to.be.calledOnceWith({ pollerName: "Add Firebase Poller", - apiOrigin: api.firebaseApiOrigin, + apiOrigin: api.firebaseApiOrigin(), apiVersion: "v1beta1", operationResourceName: OPERATION_RESOURCE_NAME_2, }); }); it("should reject if add Firebase api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); + nock(api.firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}:addFirebase`) + .reply(404); let err; try { await projectManager.addFirebaseToCloudProject(PROJECT_ID); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to add Firebase to Google Cloud Platform project. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}:addFirebase`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - } + "Failed to add Firebase to Google Cloud Platform project. See firebase-debug.log for more info.", ); + expect(err.original).to.be.an.instanceOf(FirebaseError, "Not Found"); + expect(nock.isDone()).to.be.true; expect(pollOperationStub).to.be.not.called; }); it("should reject if polling add Firebase operation throws error", async () => { const expectedError = new Error("Permission denied"); - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_2 } }); + nock(api.firebaseApiOrigin()) + .post(`/v1beta1/projects/${PROJECT_ID}:addFirebase`) + .reply(200, { name: OPERATION_RESOURCE_NAME_2 }); pollOperationStub.onFirstCall().rejects(expectedError); let err; try { await projectManager.addFirebaseToCloudProject(PROJECT_ID); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to add Firebase to Google Cloud Platform project. See firebase-debug.log for more info." + "Failed to add Firebase to Google Cloud Platform project. See firebase-debug.log for more info.", ); expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}:addFirebase`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - } - ); + expect(nock.isDone()).to.be.true; expect(pollOperationStub).to.be.calledOnceWith({ pollerName: "Add Firebase Poller", - apiOrigin: api.firebaseApiOrigin, + apiOrigin: api.firebaseApiOrigin(), apiVersion: "v1beta1", operationResourceName: OPERATION_RESOURCE_NAME_2, }); @@ -441,7 +412,7 @@ describe("Project management", () => { it("should resolve with a project page if it succeeds (no input token)", async () => { const pageSize = 10; const expectedProjectList = generateCloudProjectList(pageSize); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize }) .reply(200, { projectInfo: expectedProjectList, nextPageToken: NEXT_PAGE_TOKEN }); @@ -456,7 +427,7 @@ describe("Project management", () => { it("should resolve with a project page if it succeeds (with input token)", async () => { const pageSize = 10; const expectedProjectList = generateCloudProjectList(pageSize); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize, pageToken: PAGE_TOKEN }) .reply(200, { projectInfo: expectedProjectList, nextPageToken: NEXT_PAGE_TOKEN }); @@ -472,7 +443,7 @@ describe("Project management", () => { const pageSize = 10; const projectCounts = 5; const expectedProjectList = generateCloudProjectList(projectCounts); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize }) .reply(200, { projectInfo: expectedProjectList }); @@ -486,7 +457,7 @@ describe("Project management", () => { it("should reject if the api call fails", async () => { const pageSize = 100; - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize, pageToken: PAGE_TOKEN }) .reply(404, { error: "Not Found" }); @@ -494,12 +465,12 @@ describe("Project management", () => { let err; try { await projectManager.getAvailableCloudProjectPage(pageSize, PAGE_TOKEN); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to list available Google Cloud Platform projects. See firebase-debug.log for more info." + "Failed to list available Google Cloud Platform projects. See firebase-debug.log for more info.", ); expect(err.original.toString()).to.contain("Not Found"); expect(nock.isDone()).to.be.true; @@ -510,7 +481,7 @@ describe("Project management", () => { it("should resolve with a project page if it succeeds (no input token)", async () => { const pageSize = 10; const expectedProjectList = generateFirebaseProjectList(pageSize); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize }) .reply(200, { results: expectedProjectList, nextPageToken: NEXT_PAGE_TOKEN }); @@ -525,7 +496,7 @@ describe("Project management", () => { it("should resolve with a project page if it succeeds (with input token)", async () => { const pageSize = 10; const expectedProjectList = generateFirebaseProjectList(pageSize); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize, pageToken: PAGE_TOKEN }) .reply(200, { results: expectedProjectList, nextPageToken: NEXT_PAGE_TOKEN }); @@ -541,7 +512,7 @@ describe("Project management", () => { const pageSize = 10; const projectCounts = 5; const expectedProjectList = generateFirebaseProjectList(projectCounts); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize }) .reply(200, { results: expectedProjectList }); @@ -555,7 +526,7 @@ describe("Project management", () => { it("should reject if the api call fails", async () => { const pageSize = 100; - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize, pageToken: PAGE_TOKEN }) .reply(404, { error: "Not Found" }); @@ -563,12 +534,12 @@ describe("Project management", () => { let err; try { await projectManager.getFirebaseProjectPage(pageSize, PAGE_TOKEN); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to list Firebase projects. See firebase-debug.log for more info." + "Failed to list Firebase projects. See firebase-debug.log for more info.", ); expect(err.original.toString()).to.contain("Not Found"); expect(nock.isDone()).to.be.true; @@ -579,7 +550,7 @@ describe("Project management", () => { it("should resolve with project list if it succeeds with only 1 api call", async () => { const projectCounts = 10; const expectedProjectList = generateFirebaseProjectList(projectCounts); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 1000 }) .reply(200, { results: expectedProjectList }); @@ -595,11 +566,11 @@ describe("Project management", () => { const pageSize = 5; const nextPageToken = "next-page-token"; const expectedProjectList = generateFirebaseProjectList(projectCounts); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 5 }) .reply(200, { results: expectedProjectList.slice(0, pageSize), nextPageToken }); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 5, pageToken: nextPageToken }) .reply(200, { @@ -613,7 +584,7 @@ describe("Project management", () => { }); it("should reject if the first api call fails", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 1000 }) .reply(404, { error: "Not Found" }); @@ -621,12 +592,12 @@ describe("Project management", () => { let err; try { await projectManager.listFirebaseProjects(); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to list Firebase projects. See firebase-debug.log for more info." + "Failed to list Firebase projects. See firebase-debug.log for more info.", ); expect(err.original.toString()).to.contain("Not Found"); expect(nock.isDone()).to.be.true; @@ -637,11 +608,11 @@ describe("Project management", () => { const pageSize = 5; const nextPageToken = "next-page-token"; const expectedProjectList = generateFirebaseProjectList(projectCounts); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 5 }) .reply(200, { results: expectedProjectList.slice(0, pageSize), nextPageToken }); - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/projects") .query({ pageSize: 5, pageToken: nextPageToken }) .reply(404, { error: "Not Found" }); @@ -649,12 +620,12 @@ describe("Project management", () => { let err; try { await projectManager.listFirebaseProjects(pageSize); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( - "Failed to list Firebase projects. See firebase-debug.log for more info." + "Failed to list Firebase projects. See firebase-debug.log for more info.", ); expect(err.original.toString()).to.contain("Not Found"); expect(nock.isDone()).to.be.true; @@ -663,7 +634,7 @@ describe("Project management", () => { describe("getFirebaseProject", () => { it("should resolve with project information if it succeeds", async () => { - const expectedProjectInfo: projectManager.FirebaseProjectMetadata = { + const expectedProjectInfo: FirebaseProjectMetadata = { name: `projects/${PROJECT_ID}`, projectId: PROJECT_ID, displayName: PROJECT_NAME, @@ -675,7 +646,7 @@ describe("Project management", () => { locationId: LOCATION_ID, }, }; - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get(`/v1beta1/projects/${PROJECT_ID}`) .reply(200, expectedProjectInfo); @@ -686,20 +657,20 @@ describe("Project management", () => { }); it("should reject if the api call fails", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get(`/v1beta1/projects/${PROJECT_ID}`) .reply(404, { error: "Not Found" }); let err; try { await projectManager.getFirebaseProject(PROJECT_ID); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal( `Failed to get Firebase project ${PROJECT_ID}. ` + - "Please make sure the project exists and your account has permission to access it." + "Please make sure the project exists and your account has permission to access it.", ); expect(err.original.toString()).to.contain("Not Found"); expect(nock.isDone()).to.be.true; diff --git a/src/management/projects.ts b/src/management/projects.ts index 1c18fa1b50d..4516b434060 100644 --- a/src/management/projects.ts +++ b/src/management/projects.ts @@ -1,47 +1,20 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as ora from "ora"; import { Client } from "../apiv2"; import { FirebaseError } from "../error"; import { pollOperation } from "../operation-poller"; -import { promptOnce } from "../prompt"; -import { Question } from "inquirer"; +import { Question, promptOnce } from "../prompt"; import * as api from "../api"; import { logger } from "../logger"; import * as utils from "../utils"; +import { FirebaseProjectMetadata, CloudProjectInfo, ProjectPage } from "../types/project"; const TIMEOUT_MILLIS = 30000; const MAXIMUM_PROMPT_LIST = 100; const PROJECT_LIST_PAGE_SIZE = 1000; const CREATE_PROJECT_API_REQUEST_TIMEOUT_MILLIS = 15000; -export interface CloudProjectInfo { - project: string /* The resource name of the GCP project: "projects/projectId" */; - displayName?: string; - locationId?: string; -} - -export interface ProjectPage { - projects: T[]; - nextPageToken?: string; -} - -export interface FirebaseProjectMetadata { - name: string /* The fully qualified resource name of the Firebase project */; - projectId: string; - projectNumber: string; - displayName: string; - resources?: DefaultProjectResources; -} - -export interface DefaultProjectResources { - hostingSite?: string; - realtimeDatabaseInstance?: string; - storageBucket?: string; - locationId?: string; -} - export enum ProjectParentResourceType { ORGANIZATION = "organization", FOLDER = "folder", @@ -70,21 +43,21 @@ export const PROJECTS_CREATE_QUESTIONS: Question[] = [ ]; const firebaseAPIClient = new Client({ - urlPrefix: api.firebaseApiOrigin, + urlPrefix: api.firebaseApiOrigin(), auth: true, apiVersion: "v1beta1", }); export async function createFirebaseProjectAndLog( projectId: string, - options: { displayName?: string; parentResource?: ProjectParentResource } + options: { displayName?: string; parentResource?: ProjectParentResource }, ): Promise { const spinner = ora("Creating Google Cloud Platform project").start(); try { await createCloudProject(projectId, options); spinner.succeed(); - } catch (err) { + } catch (err: any) { spinner.fail(); throw err; } @@ -93,14 +66,14 @@ export async function createFirebaseProjectAndLog( } export async function addFirebaseToCloudProjectAndLog( - projectId: string + projectId: string, ): Promise { let projectInfo; const spinner = ora("Adding Firebase resources to Google Cloud Platform project").start(); try { projectInfo = await addFirebaseToCloudProject(projectId); - } catch (err) { + } catch (err: any) { spinner.fail(); throw err; } @@ -124,7 +97,7 @@ function logNewFirebaseProjectInfo(projectInfo: FirebaseProjectMetadata): void { logger.info(""); logger.info("Firebase console is available at"); logger.info( - `https://console.firebase.google.com/project/${clc.bold(projectInfo.projectId)}/overview` + `https://console.firebase.google.com/project/${clc.bold(projectInfo.projectId)}/overview`, ); } @@ -139,7 +112,7 @@ export async function getOrPromptProject(options: any): Promise { const { projects, nextPageToken } = await getFirebaseProjectPage(pageSize); if (projects.length === 0) { @@ -166,24 +139,24 @@ async function selectProjectByPrompting(): Promise { * Presents user with list of projects to choose from and gets project information for chosen project. */ async function selectProjectFromList( - projects: FirebaseProjectMetadata[] = [] + projects: FirebaseProjectMetadata[] = [], ): Promise { - let choices = projects + const choices = projects .filter((p: FirebaseProjectMetadata) => !!p) .map((p) => { return { name: p.projectId + (p.displayName ? ` (${p.displayName})` : ""), value: p.projectId, }; - }); - choices = _.orderBy(choices, ["name"], ["asc"]); + }) + .sort((a, b) => a.name.localeCompare(b.name)); if (choices.length >= 25) { utils.logBullet( `Don't want to scroll through all your projects? If you know your project ID, ` + `you can initialize it directly using ${clc.bold( - "firebase init --project " - )}.\n` + "firebase init --project ", + )}.\n`, ); } const projectId: string = await promptOnce({ @@ -217,7 +190,7 @@ export async function promptAvailableProjectId(): Promise { const { projects, nextPageToken } = await getAvailableCloudProjectPage(MAXIMUM_PROMPT_LIST); if (projects.length === 0) { throw new FirebaseError( - "There are no available Google Cloud projects to add Firebase services." + "There are no available Google Cloud projects to add Firebase services.", ); } @@ -228,7 +201,7 @@ export async function promptAvailableProjectId(): Promise { message: "Please input the ID of the Google Cloud Project you would like to add Firebase:", }); } else { - let choices = projects + const choices = projects .filter((p: CloudProjectInfo) => !!p) .map((p) => { const projectId = getProjectId(p); @@ -236,8 +209,8 @@ export async function promptAvailableProjectId(): Promise { name: projectId + (p.displayName ? ` (${p.displayName})` : ""), value: projectId, }; - }); - choices = _.orderBy(choices, ["name"], ["asc"]); + }) + .sort((a, b) => a.name.localeCompare(b.name)); return await promptOnce({ type: "list", name: "id", @@ -254,33 +227,38 @@ export async function promptAvailableProjectId(): Promise { */ export async function createCloudProject( projectId: string, - options: { displayName?: string; parentResource?: ProjectParentResource } + options: { displayName?: string; parentResource?: ProjectParentResource }, ): Promise { try { - const response = await api.request("POST", "/v1/projects", { - auth: true, - origin: api.resourceManagerOrigin, + const client = new Client({ urlPrefix: api.resourceManagerOrigin(), apiVersion: "v1" }); + const data = { + projectId, + name: options.displayName || projectId, + parent: options.parentResource, + }; + const response = await client.request({ + method: "POST", + path: "/projects", + body: data, timeout: CREATE_PROJECT_API_REQUEST_TIMEOUT_MILLIS, - data: { projectId, name: options.displayName || projectId, parent: options.parentResource }, }); - const projectInfo = await pollOperation({ pollerName: "Project Creation Poller", - apiOrigin: api.resourceManagerOrigin, + apiOrigin: api.resourceManagerOrigin(), apiVersion: "v1", operationResourceName: response.body.name /* LRO resource name */, }); return projectInfo; - } catch (err) { + } catch (err: any) { if (err.status === 409) { throw new FirebaseError( `Failed to create project because there is already a project with ID ${clc.bold( - projectId + projectId, )}. Please try again with a unique project ID.`, { exit: 2, original: err, - } + }, ); } else { throw new FirebaseError("Failed to create project. See firebase-debug.log for more info.", { @@ -297,26 +275,26 @@ export async function createCloudProject( * @return a promise that resolves to the new firebase project information */ export async function addFirebaseToCloudProject( - projectId: string + projectId: string, ): Promise { try { - const response = await api.request("POST", `/v1beta1/projects/${projectId}:addFirebase`, { - auth: true, - origin: api.firebaseApiOrigin, + const response = await firebaseAPIClient.request({ + method: "POST", + path: `/projects/${projectId}:addFirebase`, timeout: CREATE_PROJECT_API_REQUEST_TIMEOUT_MILLIS, }); const projectInfo = await pollOperation({ pollerName: "Add Firebase Poller", - apiOrigin: api.firebaseApiOrigin, + apiOrigin: api.firebaseApiOrigin(), apiVersion: "v1beta1", operationResourceName: response.body.name /* LRO resource name */, }); return projectInfo; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( "Failed to add Firebase to Google Cloud Platform project. See firebase-debug.log for more info.", - { exit: 2, original: err } + { exit: 2, original: err }, ); } } @@ -327,7 +305,7 @@ async function getProjectPage( responseKey: string; // The list is located at "apiResponse.body[responseKey]" pageSize: number; pageToken?: string; - } + }, ): Promise> { const queryParams: { [key: string]: string } = { pageSize: `${options.pageSize}`, @@ -356,7 +334,7 @@ async function getProjectPage( */ export async function getFirebaseProjectPage( pageSize: number = PROJECT_LIST_PAGE_SIZE, - pageToken?: string + pageToken?: string, ): Promise> { let projectPage; @@ -366,11 +344,11 @@ export async function getFirebaseProjectPage( pageSize, pageToken, }); - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( "Failed to list Firebase projects. See firebase-debug.log for more info.", - { exit: 2, original: err } + { exit: 2, original: err }, ); } @@ -383,7 +361,7 @@ export async function getFirebaseProjectPage( */ export async function getAvailableCloudProjectPage( pageSize: number = PROJECT_LIST_PAGE_SIZE, - pageToken?: string + pageToken?: string, ): Promise> { try { return await getProjectPage("/availableProjects", { @@ -391,11 +369,11 @@ export async function getAvailableCloudProjectPage( pageSize, pageToken, }); - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( "Failed to list available Google Cloud Platform projects. See firebase-debug.log for more info.", - { exit: 2, original: err } + { exit: 2, original: err }, ); } } @@ -412,7 +390,7 @@ export async function listFirebaseProjects(pageSize?: number): Promise = await getFirebaseProjectPage( pageSize, - nextPageToken + nextPageToken, ); projects.push(...projectPage.projects); nextPageToken = projectPage.nextPageToken; @@ -432,12 +410,16 @@ export async function getFirebaseProject(projectId: string): Promise { + it("can calcluate recursive keys", () => { + // BUG BUG BUG: String literals seem to extend each other? I can break the + // test and it still passes. + const test: SameType< + RecursiveKeyOf<{ + a: number; + b: { + c: boolean; + d: { + e: number; + }; + f: Array<{ g: number }>; + }; + }>, + "a" | "a.b" | "a.b.c" | "a.b.d" | "a.b.d.e" | "a.b.f.g" + > = true; + expect(test).to.be.true; + }); + + it("can detect recursive elems", () => { + const test: SameType, "a" | "b" | "c"> = true; + expect(test).to.be.true; + }); + + it("Can deep pick", () => { + interface original { + a: number; + b: { + c?: boolean; + d: { + e: number; + }; + g: boolean; + }; + c?: { + d: number; + }; + h?: number; + } + + interface expected { + a: number; + b: { + c?: boolean; + }; + c?: { + d: number; + }; + h?: number; + } + + const test: SameType, expected> = true; + expect(test).to.be.true; + }); + + it("can deep omit", () => { + interface original { + a: number; + b: { + c: boolean; + d: { + e?: number; + }; + g: boolean; + }; + h?: number; + g: number; + } + + interface expected { + b: { + d: { + e?: number; + }; + g: boolean; + }; + h?: number; + g: number; + } + + const test: SameType, expected> = true; + expect(test).to.be.true; + }); + + it("can require keys", () => { + interface original { + a?: number; + b?: number; + } + + interface expected { + a: number; + b?: number; + } + + const test: SameType, expected> = true; + expect(test).to.be.true; + }); + + it("Can DeepExtract", () => { + type test = "a" | "b.c" | "b.d.e" | "b.d.f" | "b.g"; + type extract = "a" | "b.c" | "b.d"; + type expected = "a" | "b.c" | "b.d.e" | "b.d.f"; + const test: SameType, expected> = true; + }); +}); diff --git a/src/metaprogramming.ts b/src/metaprogramming.ts new file mode 100644 index 00000000000..8d7d53a541f --- /dev/null +++ b/src/metaprogramming.ts @@ -0,0 +1,118 @@ +// eslint-disable-next-line @typescript-eslint/ban-types +type Primitive = number | string | null | undefined | Date | Function; + +/** + * Statically verify that one type implements another. + * This is very useful to say assertImplements>(); + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function +export function assertImplements(): void {} + +/** + * RecursiveKeyOf is a type for keys of an objet usind dots for subfields. + * For a given object: {a: {b: {c: number}}, d } the RecursiveKeysOf are + * 'a' | 'a.b' | 'a.b.c' | 'd' + */ +export type RecursiveKeyOf = T extends Primitive + ? never + : T extends (infer Elem)[] + ? RecursiveSubKeys + : + | (keyof T & string) + | { + [P in keyof Required & string]: RecursiveSubKeys, P>; + }[keyof T & string]; + +type RecursiveSubKeys = T[P] extends (infer Elem)[] + ? `${P}.${RecursiveKeyOf}` + : T[P] extends object + ? `${P}.${RecursiveKeyOf}` + : never; + +export type DeepExtract = [ + RecursiveKeys extends `${infer Head}.${infer Rest}` + ? Head extends Select + ? Head + : DeepExtract, Rest> + : Extract, +][number]; + +/** + * SameType is used in testing to verify that two types are the same. + * Usage: + * const test: SameType = true. + * The assigment will fail if the types are different. + */ +export type SameType = T extends V ? (V extends T ? true : false) : false; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type HeadOf = [T extends `${infer Head}.${infer Tail}` ? Head : T][number]; + +type TailsOf = [ + T extends `${Head}.${infer Tail}` ? Tail : never, +][number]; + +type RequiredFields = { + [K in keyof T as (object extends Pick ? never : K) & string]: T[K]; +}; + +type OptionalFields = { + [K in keyof T as (object extends Pick ? K : never) & string]?: T[K]; +}; + +/** + * DeepOmit allows you to omit fields from a nested structure using recursive keys. + */ +export type DeepOmit> = DeepOmitUnsafe; + +type DeepOmitUnsafe = T extends (infer Elem)[] + ? Array> + : { + [Key in Exclude, Keys>]: Key extends HeadOf + ? DeepOmitUnsafe> + : T[Key]; + } & { + [Key in Exclude, Keys>]?: Key extends HeadOf + ? DeepOmitUnsafe> + : T[Key]; + }; + +export type DeepPick> = DeepPickUnsafe; + +type DeepPickUnsafe = T extends (infer Elem)[] + ? Array> + : { + [Key in Extract, HeadOf>]: Key extends Keys + ? T[Key] + : DeepPickUnsafe>; + } & { + [Key in Extract, HeadOf>]?: Key extends Keys + ? T[Key] + : DeepPickUnsafe>; + }; + +/** + * Make properties of an object required. + * + * type Foo = { + * a?: string + * b?: number + * c?: object + * } + * + * type Bar = RequireKeys + * // Property "a" and "b" are now required. + */ +export type RequireKeys = T & Required>; + +/** In the array LeafElems<[[["a"], "b"], ["c"]]> is "a" | "b" | "c" */ +export type LeafElems = + T extends Array ? (Elem extends unknown[] ? LeafElems : Elem) : T; + +/** + * In the object {a: number, b: { c: string } }, + * LeafValues is number | string + */ +export type LeafValues = { + [Key in keyof T & string]: T[Key] extends object ? LeafValues : T[Key]; +}[keyof T & string]; diff --git a/src/test/operation-poller.spec.ts b/src/operation-poller.spec.ts similarity index 93% rename from src/test/operation-poller.spec.ts rename to src/operation-poller.spec.ts index b6c118d56db..1a9a9121cf4 100644 --- a/src/test/operation-poller.spec.ts +++ b/src/operation-poller.spec.ts @@ -2,9 +2,9 @@ import { expect } from "chai"; import * as nock from "nock"; import * as sinon from "sinon"; -import { FirebaseError } from "../error"; -import { OperationPollerOptions, pollOperation } from "../operation-poller"; -import TimeoutError from "../throttler/errors/timeout-error"; +import { FirebaseError } from "./error"; +import { OperationPollerOptions, pollOperation } from "./operation-poller"; +import TimeoutError from "./throttler/errors/timeout-error"; const TEST_ORIGIN = "https://firebasedummy.googleapis.com.com"; const VERSION = "v1"; @@ -51,7 +51,7 @@ describe("OperationPoller", () => { let err; try { await pollOperation(pollerOptions); - } catch (e) { + } catch (e: any) { err = e; } expect(err.message).to.equal("failed"); @@ -64,7 +64,7 @@ describe("OperationPoller", () => { await expect(pollOperation(pollerOptions)).to.eventually.be.rejectedWith( FirebaseError, - "404" + "404", ); expect(nock.isDone()).to.be.true; }); @@ -93,7 +93,7 @@ describe("OperationPoller", () => { let error; try { await pollOperation(pollerOptions); - } catch (err) { + } catch (err: any) { error = err; } expect(error).to.be.instanceOf(TimeoutError); @@ -104,7 +104,7 @@ describe("OperationPoller", () => { const opResult = { done: true, response: "completed" }; nock(TEST_ORIGIN).get(FULL_RESOURCE_NAME).reply(200, { done: false }); nock(TEST_ORIGIN).get(FULL_RESOURCE_NAME).reply(200, opResult); - const onPollSpy = sinon.spy((op: any) => { + const onPollSpy = sinon.spy(() => { return; }); pollerOptions.onPoll = onPollSpy; diff --git a/src/operation-poller.ts b/src/operation-poller.ts index d908de5b32f..2f13d92e195 100644 --- a/src/operation-poller.ts +++ b/src/operation-poller.ts @@ -2,6 +2,17 @@ import { Client } from "./apiv2"; import { FirebaseError } from "./error"; import { Queue } from "./throttler/queue"; +export interface LongRunningOperation { + // The identifier of the Operation. + readonly name: string; + + // Set to `true` if the Operation is done. + readonly done: boolean; + + // Additional metadata about the Operation. + readonly metadata: T | undefined; +} + export interface OperationPollerOptions { pollerName?: string; apiOrigin: string; @@ -11,6 +22,7 @@ export interface OperationPollerOptions { maxBackoff?: number; masterTimeout?: number; onPoll?: (operation: OperationResult) => any; + doneFn?: (op: any) => boolean; } const DEFAULT_INITIAL_BACKOFF_DELAY_MILLIS = 250; @@ -20,8 +32,10 @@ export interface OperationResult { done?: boolean; response?: T; error?: { + name: string; message: string; code: number; + details?: any[]; }; metadata?: { [key: string]: any; @@ -51,7 +65,7 @@ export class OperationPoller { if (error) { throw error instanceof FirebaseError ? error - : new FirebaseError(error.message, { status: error.code }); + : new FirebaseError(error.message, { status: error.code, original: error }); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return response!; @@ -67,7 +81,7 @@ export class OperationPoller { let res; try { res = await apiClient.get>(options.operationResourceName); - } catch (err) { + } catch (err: any) { // Responses with 500 or 503 status code are treated as retriable errors. if (err.status === 500 || err.status === 503) { throw err; @@ -77,7 +91,12 @@ export class OperationPoller { if (options.onPoll) { options.onPoll(res.body); } - if (!res.body.done) { + if (options.doneFn) { + const done = options.doneFn(res.body); + if (!done) { + throw new Error("Polling incomplete, should trigger retry with backoff"); + } + } else if (!res.body.done) { throw new Error("Polling incomplete, should trigger retry with backoff"); } return res.body; diff --git a/src/options.ts b/src/options.ts index dfd50a29eb1..45814867e0c 100644 --- a/src/options.ts +++ b/src/options.ts @@ -3,7 +3,7 @@ import { RC } from "./rc"; // Options come from command-line options and stored config values // TODO: actually define all of this stuff in command.ts and import it from there. -export interface Options { +export interface BaseOptions { cwd: string; configPath: string; only: string; @@ -17,6 +17,7 @@ export interface Options { projectAlias?: string; projectId?: string; projectNumber?: string; + projectRoot?: string; account?: string; json: boolean; nonInteractive: boolean; @@ -24,7 +25,15 @@ export interface Options { debug: boolean; rc: RC; + // Emulator specific import/export options + exportOnExit?: boolean | string; + import?: string; +} +export interface Options extends BaseOptions { // TODO(samstern): Remove this once options is better typed [key: string]: unknown; + + // whether it's coming from the VS Code Extension + isVSCE?: true; } diff --git a/src/parseBoltRules.js b/src/parseBoltRules.js deleted file mode 100644 index 6db9675dec8..00000000000 --- a/src/parseBoltRules.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; - -var fs = require("fs"); -var spawn = require("cross-spawn"); -var { FirebaseError } = require("./error"); -var clc = require("cli-color"); -var _ = require("lodash"); - -module.exports = function (filename) { - var ruleSrc = fs.readFileSync(filename, "utf8"); - - // Use 'npx' to spawn 'firebase-bolt' so that it can be picked up - // from either a global install or from local ./node_modules/ - var result = spawn.sync("npx", ["--no-install", "firebase-bolt"], { - input: ruleSrc, - timeout: 10000, - encoding: "utf-8", - }); - - if (result.error && _.get(result.error, "code") === "ENOENT") { - throw new FirebaseError("Bolt not installed, run " + clc.bold("npm install -g firebase-bolt"), { - exit: 1, - }); - } else if (result.error) { - throw new FirebaseError("Unexpected error parsing Bolt rules file", { - exit: 2, - }); - } else if (result.status != null && result.status > 0) { - throw new FirebaseError(result.stderr.toString(), { exit: 1 }); - } - - return result.stdout; -}; diff --git a/src/parseBoltRules.ts b/src/parseBoltRules.ts new file mode 100644 index 00000000000..5ab233034ae --- /dev/null +++ b/src/parseBoltRules.ts @@ -0,0 +1,30 @@ +import * as fs from "fs"; +import * as spawn from "cross-spawn"; +import * as clc from "colorette"; +import * as _ from "lodash"; + +import { FirebaseError } from "./error"; + +export function parseBoltRules(filename: string): string { + const ruleSrc = fs.readFileSync(filename, "utf8"); + + // Use 'npx' to spawn 'firebase-bolt' so that it can be picked up + // from either a global install or from local ./node_modules/ + const result = spawn.sync("npx", ["--no-install", "firebase-bolt"], { + input: ruleSrc, + timeout: 10000, + encoding: "utf-8", + }); + + if (result.error && _.get(result.error, "code") === "ENOENT") { + throw new FirebaseError("Bolt not installed, run " + clc.bold("npm install -g firebase-bolt")); + } else if (result.error) { + throw new FirebaseError("Unexpected error parsing Bolt rules file", { + exit: 2, + }); + } else if (result.status != null && result.status > 0) { + throw new FirebaseError(result.stderr.toString(), { exit: 1 }); + } + + return result.stdout; +} diff --git a/src/prepareFirebaseRules.js b/src/prepareFirebaseRules.js deleted file mode 100644 index 20f7ffd160c..00000000000 --- a/src/prepareFirebaseRules.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; - -var clc = require("cli-color"); -var fs = require("fs"); - -var api = require("./api"); -var utils = require("./utils"); - -var prepareFirebaseRules = function (component, options, payload) { - var rulesFileName = component + ".rules"; - var rulesPath = options.config.get(rulesFileName); - if (rulesPath) { - rulesPath = options.config.path(rulesPath); - var src = fs.readFileSync(rulesPath, "utf8"); - utils.logBullet(clc.bold.cyan(component + ":") + " checking rules for compilation errors..."); - return api - .request("POST", "/v1/projects/" + encodeURIComponent(options.project) + ":test", { - origin: api.rulesOrigin, - data: { - source: { - files: [ - { - content: src, - name: rulesFileName, - }, - ], - }, - }, - auth: true, - }) - .then(function (response) { - if (response.body && response.body.issues && response.body.issues.length > 0) { - var add = response.body.issues.length === 1 ? "" : "s"; - var message = - "Compilation error" + - add + - " in " + - clc.bold(options.config.get(rulesFileName)) + - ":\n"; - response.body.issues.forEach(function (issue) { - message += - "\n[" + - issue.severity.substring(0, 1) + - "] " + - issue.sourcePosition.line + - ":" + - issue.sourcePosition.column + - " - " + - issue.description; - }); - - return utils.reject(message, { exit: 1 }); - } - - utils.logSuccess(clc.bold.green(component + ":") + " rules file compiled successfully"); - payload[component] = { - rules: [{ name: options.config.get(rulesFileName), content: src }], - }; - return Promise.resolve(); - }); - } - - return Promise.resolve(); -}; - -module.exports = prepareFirebaseRules; diff --git a/src/prepareUpload.js b/src/prepareUpload.js deleted file mode 100644 index 18cefe6a796..00000000000 --- a/src/prepareUpload.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; - -var fs = require("fs"); -var path = require("path"); - -var tar = require("tar"); -var tmp = require("tmp"); - -var { listFiles } = require("./listFiles"); -var { FirebaseError } = require("./error"); -var fsutils = require("./fsutils"); - -module.exports = function (options) { - var hostingConfig = options.config.get("hosting"); - var publicDir = options.config.path(hostingConfig.public); - var indexPath = path.join(publicDir, "index.html"); - - var tmpFile = tmp.fileSync({ - prefix: "firebase-upload-", - postfix: ".tar.gz", - }); - var manifest = listFiles(publicDir, hostingConfig.ignore); - - return tar - .c( - { - gzip: true, - file: tmpFile.name, - cwd: publicDir, - prefix: "public", - follow: true, - noDirRecurse: true, - portable: true, - }, - manifest.slice(0) - ) - .then(function () { - var stats = fs.statSync(tmpFile.name); - return { - file: tmpFile.name, - stream: fs.createReadStream(tmpFile.name), - manifest: manifest, - foundIndex: fsutils.fileExistsSync(indexPath), - size: stats.size, - }; - }) - .catch(function (err) { - return Promise.reject( - new FirebaseError("There was an issue preparing Hosting files for upload.", { - original: err, - exit: 2, - }) - ); - }); -}; diff --git a/src/previews.ts b/src/previews.ts deleted file mode 100644 index f135f667665..00000000000 --- a/src/previews.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { has, set } from "lodash"; -import { configstore } from "./configstore"; - -interface PreviewFlags { - rtdbrules: boolean; - ext: boolean; - extdev: boolean; - rtdbmanagement: boolean; - functionsv2: boolean; - golang: boolean; - deletegcfartifacts: boolean; - dotenv: boolean; - artifactregistry: boolean; -} - -export const previews: PreviewFlags = { - // insert previews here... - rtdbrules: false, - ext: false, - extdev: false, - rtdbmanagement: false, - functionsv2: false, - golang: false, - deletegcfartifacts: false, - dotenv: false, - artifactregistry: false, - - ...(configstore.get("previews") as Partial), -}; - -if (process.env.FIREBASE_CLI_PREVIEWS) { - process.env.FIREBASE_CLI_PREVIEWS.split(",").forEach((feature) => { - if (has(previews, feature)) { - set(previews, feature, true); - } - }); -} diff --git a/src/profileReport.js b/src/profileReport.js deleted file mode 100644 index edfa2e78da2..00000000000 --- a/src/profileReport.js +++ /dev/null @@ -1,676 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -"use strict"; - -var clc = require("cli-color"); -var Table = require("cli-table"); -var fs = require("fs"); -var _ = require("lodash"); -var readline = require("readline"); - -var { FirebaseError } = require("./error"); -const { logger } = require("./logger"); - -var DATA_LINE_REGEX = /^data: /; - -var BANDWIDTH_NOTE = - "NOTE: The numbers reported here are only estimates of the data" + - " payloads from read operations. They are NOT a valid measure of your bandwidth bill."; - -var SPEED_NOTE = - "NOTE: Speeds are reported at millisecond resolution and" + - " are not the latencies that clients will see. Pending times" + - " are also reported at millisecond resolution. They approximate" + - " the interval of time between the instant a request is received" + - " and the instant it executes."; - -var COLLAPSE_THRESHOLD = 25; -var COLLAPSE_WILDCARD = ["$wildcard"]; - -/** - * @constructor - * @this ProfileReport - */ -var ProfileReport = function (tmpFile, outStream, options) { - this.tempFile = tmpFile; - this.output = outStream; - this.options = options; - this.state = { - outband: {}, - inband: {}, - writeSpeed: {}, - broadcastSpeed: {}, - readSpeed: {}, - connectSpeed: {}, - disconnectSpeed: {}, - unlistenSpeed: {}, - unindexed: {}, - startTime: 0, - endTime: 0, - opCount: 0, - }; -}; - -// 'static' helper methods - -ProfileReport.extractJSON = function (line, input) { - if (!input && !DATA_LINE_REGEX.test(line)) { - return null; - } else if (!input) { - line = line.substring(5); - } - try { - return JSON.parse(line); - } catch (e) { - return null; - } -}; - -ProfileReport.pathString = function (path) { - return "/" + (path ? path.join("/") : ""); -}; - -ProfileReport.formatNumber = function (num) { - var parts = num.toFixed(2).split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - if (+parts[1] === 0) { - return parts[0]; - } - return parts.join("."); -}; - -ProfileReport.formatBytes = function (bytes) { - var threshold = 1000; - if (Math.round(bytes) < threshold) { - return bytes + " B"; - } - var units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - var u = -1; - var formattedBytes = bytes; - do { - formattedBytes /= threshold; - u++; - } while (Math.abs(formattedBytes) >= threshold && u < units.length - 1); - return ProfileReport.formatNumber(formattedBytes) + " " + units[u]; -}; - -ProfileReport.extractReadableIndex = function (query) { - if (_.has(query, "orderBy")) { - return query.orderBy; - } - var indexPath = _.get(query, "index.path"); - if (indexPath) { - return ProfileReport.pathString(indexPath); - } - return ".value"; -}; - -ProfileReport.prototype.collectUnindexed = function (data, path) { - if (!data.unIndexed) { - return; - } - if (!_.has(this.state.unindexed, path)) { - this.state.unindexed[path] = {}; - } - var pathNode = this.state.unindexed[path]; - // There is only ever one query. - var query = data.querySet[0]; - // Get a unique string for this query. - var index = JSON.stringify(query.index); - if (!_.has(pathNode, index)) { - pathNode[index] = { - times: 0, - query: query, - }; - } - var indexNode = pathNode[index]; - indexNode.times += 1; -}; - -ProfileReport.prototype.collectSpeedUnpathed = function (data, opStats) { - if (Object.keys(opStats).length === 0) { - opStats.times = 0; - opStats.millis = 0; - opStats.pendingCount = 0; - opStats.pendingTime = 0; - opStats.rejected = 0; - } - opStats.times += 1; - - if (data.hasOwnProperty("millis")) { - opStats.millis += data.millis; - } - if (data.hasOwnProperty("pendingTime")) { - opStats.pendingCount++; - opStats.pendingTime += data.pendingTime; - } - // Explictly check for false, in case its not defined. - if (data.allowed === false) { - opStats.rejected += 1; - } -}; - -ProfileReport.prototype.collectSpeed = function (data, path, opType) { - if (!_.has(opType, path)) { - opType[path] = { - times: 0, - millis: 0, - pendingCount: 0, - pendingTime: 0, - rejected: 0, - }; - } - var node = opType[path]; - node.times += 1; - /* - * If `millis` is not present, we assume that the operation is fast - * in-memory request that is not timed on the server-side (e.g. - * connects, disconnects, listens, unlistens). Such a request may - * have non-trivial `pendingTime`. - */ - if (data.hasOwnProperty("millis")) { - node.millis += data.millis; - } - if (data.hasOwnProperty("pendingTime")) { - node.pendingCount++; - node.pendingTime += data.pendingTime; - } - // Explictly check for false, in case its not defined. - if (data.allowed === false) { - node.rejected += 1; - } -}; - -ProfileReport.prototype.collectBandwidth = function (bytes, path, direction) { - if (!_.has(direction, path)) { - direction[path] = { - times: 0, - bytes: 0, - }; - } - var node = direction[path]; - node.times += 1; - node.bytes += bytes; -}; - -ProfileReport.prototype.collectRead = function (data, path, bytes) { - this.collectSpeed(data, path, this.state.readSpeed); - this.collectBandwidth(bytes, path, this.state.outband); -}; - -ProfileReport.prototype.collectBroadcast = function (data, path, bytes) { - this.collectSpeed(data, path, this.state.broadcastSpeed); - this.collectBandwidth(bytes, path, this.state.outband); -}; - -ProfileReport.prototype.collectUnlisten = function (data, path) { - this.collectSpeed(data, path, this.state.unlistenSpeed); -}; - -ProfileReport.prototype.collectConnect = function (data) { - this.collectSpeedUnpathed(data, this.state.connectSpeed); -}; - -ProfileReport.prototype.collectDisconnect = function (data) { - this.collectSpeedUnpathed(data, this.state.disconnectSpeed); -}; - -ProfileReport.prototype.collectWrite = function (data, path, bytes) { - this.collectSpeed(data, path, this.state.writeSpeed); - this.collectBandwidth(bytes, path, this.state.inband); -}; - -ProfileReport.prototype.processOperation = function (data) { - if (!this.state.startTime) { - this.state.startTime = data.timestamp; - } - this.state.endTime = data.timestamp; - var path = ProfileReport.pathString(data.path); - this.state.opCount++; - switch (data.name) { - case "concurrent-connect": - this.collectConnect(data); - break; - case "concurrent-disconnect": - this.collectDisconnect(data); - break; - case "realtime-read": - this.collectRead(data, path, data.bytes); - break; - case "realtime-write": - this.collectWrite(data, path, data.bytes); - break; - case "realtime-transaction": - this.collectWrite(data, path, data.bytes); - break; - case "realtime-update": - this.collectWrite(data, path, data.bytes); - break; - case "listener-listen": - this.collectRead(data, path, data.bytes); - this.collectUnindexed(data, path); - break; - case "listener-broadcast": - this.collectBroadcast(data, path, data.bytes); - break; - case "listener-unlisten": - this.collectUnlisten(data, path); - break; - case "rest-read": - this.collectRead(data, path, data.bytes); - break; - case "rest-write": - this.collectWrite(data, path, data.bytes); - break; - case "rest-update": - this.collectWrite(data, path, data.bytes); - break; - default: - break; - } -}; - -/** - * Takes an object with keys that are paths and combines the - * keys that have similar prefixes. - * Combining is done via the combiner function. - */ -ProfileReport.prototype.collapsePaths = function (pathedObject, combiner, pathIndex) { - if (!this.options.collapse) { - // Don't do this if the --no-collapse flag is specified - return pathedObject; - } - if (_.isUndefined(pathIndex)) { - pathIndex = 1; - } - var allSegments = _.keys(pathedObject).map(function (path) { - return path.split("/").filter(function (s) { - return s !== ""; - }); - }); - var pathSegments = allSegments.filter(function (segments) { - return segments.length > pathIndex; - }); - var otherSegments = allSegments.filter(function (segments) { - return segments.length <= pathIndex; - }); - if (pathSegments.length === 0) { - return pathedObject; - } - var prefixes = {}; - // Count path prefixes for the index. - pathSegments.forEach(function (segments) { - var prefixPath = ProfileReport.pathString(segments.slice(0, pathIndex)); - var prefixCount = _.get(prefixes, prefixPath, new Set()); - prefixes[prefixPath] = prefixCount.add(segments[pathIndex]); - }); - var collapsedObject = {}; - pathSegments.forEach(function (segments) { - var prefix = segments.slice(0, pathIndex); - var prefixPath = ProfileReport.pathString(prefix); - var prefixCount = _.get(prefixes, prefixPath); - var originalPath = ProfileReport.pathString(segments); - if (prefixCount.size >= COLLAPSE_THRESHOLD) { - var tail = segments.slice(pathIndex + 1); - var collapsedPath = ProfileReport.pathString(prefix.concat(COLLAPSE_WILDCARD).concat(tail)); - var currentValue = collapsedObject[collapsedPath]; - if (currentValue) { - collapsedObject[collapsedPath] = combiner(currentValue, pathedObject[originalPath]); - } else { - collapsedObject[collapsedPath] = pathedObject[originalPath]; - } - } else { - collapsedObject[originalPath] = pathedObject[originalPath]; - } - }); - otherSegments.forEach(function (segments) { - var originalPath = ProfileReport.pathString(segments); - collapsedObject[originalPath] = pathedObject[originalPath]; - }); - // Do this again, but down a level. - return this.collapsePaths(collapsedObject, combiner, pathIndex + 1); -}; - -ProfileReport.prototype.renderUnindexedData = function () { - var table = new Table({ - head: ["Path", "Index", "Count"], - style: { - head: this.options.isFile ? [] : ["yellow"], - border: this.options.isFile ? [] : ["grey"], - }, - }); - var unindexed = this.collapsePaths(this.state.unindexed, function (u1, u2) { - _.mergeWith(u1, u2, function (p1, p2) { - return { - times: p1.times + p2.times, - query: p1.query, - }; - }); - }); - var paths = _.keys(unindexed); - paths.forEach(function (path) { - var indices = _.keys(unindexed[path]); - indices.forEach(function (index) { - var data = unindexed[path][index]; - var row = [ - path, - ProfileReport.extractReadableIndex(data.query), - ProfileReport.formatNumber(data.times), - ]; - table.push(row); - }); - }); - return table; -}; - -ProfileReport.prototype.renderBandwidth = function (pureData) { - var table = new Table({ - head: ["Path", "Total", "Count", "Average"], - style: { - head: this.options.isFile ? [] : ["yellow"], - border: this.options.isFile ? [] : ["grey"], - }, - }); - var data = this.collapsePaths(pureData, function (b1, b2) { - return { - bytes: b1.bytes + b2.bytes, - times: b1.times + b2.times, - }; - }); - var paths = _.keys(data); - paths = _.orderBy( - paths, - function (path) { - var bandwidth = data[path]; - return bandwidth.bytes; - }, - ["desc"] - ); - paths.forEach(function (path) { - var bandwidth = data[path]; - var row = [ - path, - ProfileReport.formatBytes(bandwidth.bytes), - ProfileReport.formatNumber(bandwidth.times), - ProfileReport.formatBytes(bandwidth.bytes / bandwidth.times), - ]; - table.push(row); - }); - return table; -}; - -ProfileReport.prototype.renderOutgoingBandwidth = function () { - return this.renderBandwidth(this.state.outband); -}; - -ProfileReport.prototype.renderIncomingBandwidth = function () { - return this.renderBandwidth(this.state.inband); -}; - -/* - * Some Realtime Database operations (concurrent-connect, concurrent-disconnect) - * are not logically associated with a path in the database. In this source - * file, we associate these operations with the sentinel path "null" so that - * they can still be aggregated in `collapsePaths`. So as to not confuse - * developers, we render aggregate statistics for such operations without a - * `path` table column. - */ -ProfileReport.prototype.renderUnpathedOperationSpeed = function (speedData, hasSecurity) { - var head = ["Count", "Average Execution Speed", "Average Pending Time"]; - if (hasSecurity) { - head.push("Permission Denied"); - } - var table = new Table({ - head: head, - style: { - head: this.options.isFile ? [] : ["yellow"], - border: this.options.isFile ? [] : ["grey"], - }, - }); - /* - * If no unpathed opeartion was seen, the corresponding stats sub-object will - * be empty. - */ - if (Object.keys(speedData).length > 0) { - var row = [ - speedData.times, - ProfileReport.formatNumber(speedData.millis / speedData.times) + " ms", - ProfileReport.formatNumber( - speedData.pendingCount === 0 ? 0 : speedData.pendingTime / speedData.pendingCount - ) + " ms", - ]; - if (hasSecurity) { - row.push(ProfileReport.formatNumber(speedData.rejected)); - } - table.push(row); - } - return table; -}; - -ProfileReport.prototype.renderOperationSpeed = function (pureData, hasSecurity) { - var head = ["Path", "Count", "Average Execution Speed", "Average Pending Time"]; - if (hasSecurity) { - head.push("Permission Denied"); - } - var table = new Table({ - head: head, - style: { - head: this.options.isFile ? [] : ["yellow"], - border: this.options.isFile ? [] : ["grey"], - }, - }); - var data = this.collapsePaths(pureData, function (s1, s2) { - return { - times: s1.times + s2.times, - millis: s1.millis + s2.millis, - pendingCount: s1.pendingCount + s2.pendingCount, - pendingTime: s1.pendingTime + s2.pendingTime, - rejected: s1.rejected + s2.rejected, - }; - }); - var paths = _.keys(data); - paths = _.orderBy( - paths, - function (path) { - var speed = data[path]; - return speed.millis / speed.times; - }, - ["desc"] - ); - paths.forEach(function (path) { - var speed = data[path]; - var row = [ - path, - speed.times, - ProfileReport.formatNumber(speed.millis / speed.times) + " ms", - ProfileReport.formatNumber( - speed.pendingCount === 0 ? 0 : speed.pendingTime / speed.pendingCount - ) + " ms", - ]; - if (hasSecurity) { - row.push(ProfileReport.formatNumber(speed.rejected)); - } - table.push(row); - }); - return table; -}; - -ProfileReport.prototype.renderReadSpeed = function () { - return this.renderOperationSpeed(this.state.readSpeed, true); -}; - -ProfileReport.prototype.renderWriteSpeed = function () { - return this.renderOperationSpeed(this.state.writeSpeed, true); -}; - -ProfileReport.prototype.renderBroadcastSpeed = function () { - return this.renderOperationSpeed(this.state.broadcastSpeed, false); -}; - -ProfileReport.prototype.renderConnectSpeed = function () { - return this.renderUnpathedOperationSpeed(this.state.connectSpeed, false); -}; - -ProfileReport.prototype.renderDisconnectSpeed = function () { - return this.renderUnpathedOperationSpeed(this.state.disconnectSpeed, false); -}; - -ProfileReport.prototype.renderUnlistenSpeed = function () { - return this.renderOperationSpeed(this.state.unlistenSpeed, false); -}; - -ProfileReport.prototype.parse = function (onLine, onClose) { - var isFile = this.options.isFile; - var tmpFile = this.tempFile; - var outStream = this.output; - var isInput = this.options.isInput; - return new Promise(function (resolve, reject) { - var rl = readline.createInterface({ - input: fs.createReadStream(tmpFile), - }); - var errored = false; - rl.on("line", function (line) { - var data = ProfileReport.extractJSON(line, isInput); - if (!data) { - return; - } - onLine(data); - }); - rl.on("close", function () { - if (errored) { - reject(new FirebaseError("There was an error creating the report.")); - } else { - var result = onClose(); - if (isFile) { - // Only resolve once the data is flushed. - outStream.on("finish", function () { - resolve(result); - }); - outStream.end(); - } else { - resolve(result); - } - } - }); - rl.on("error", function () { - reject(); - }); - outStream.on("error", function () { - errored = true; - rl.close(); - }); - }); -}; - -ProfileReport.prototype.write = function (data) { - if (this.options.isFile) { - this.output.write(data); - } else { - logger.info(data); - } -}; - -ProfileReport.prototype.generate = function () { - if (this.options.format === "TXT") { - return this.generateText(); - } else if (this.options.format === "RAW") { - return this.generateRaw(); - } else if (this.options.format === "JSON") { - return this.generateJson(); - } - throw new FirebaseError('Invalid report format expected "TXT", "JSON", or "RAW"', { - exit: 1, - }); -}; - -ProfileReport.prototype.generateRaw = function () { - return this.parse(this.writeRaw.bind(this), function () { - return null; - }); -}; - -ProfileReport.prototype.writeRaw = function (data) { - // Just write the json to the output - this.write(JSON.stringify(data) + "\n"); -}; - -ProfileReport.prototype.generateText = function () { - return this.parse(this.processOperation.bind(this), this.outputText.bind(this)); -}; - -ProfileReport.prototype.outputText = function () { - var totalTime = this.state.endTime - this.state.startTime; - var isFile = this.options.isFile; - var write = this.write.bind(this); - var writeTitle = function (title) { - if (isFile) { - write(title + "\n"); - } else { - write(clc.bold.yellow(title) + "\n"); - } - }; - var writeTable = function (title, table) { - writeTitle(title); - write(table.toString() + "\n"); - }; - writeTitle( - "Report operations collected from " + - new Date(this.state.startTime).toISOString() + - " over " + - totalTime + - " ms." - ); - writeTitle("Speed Report\n"); - write(SPEED_NOTE + "\n\n"); - writeTable("Read Speed", this.renderReadSpeed()); - writeTable("Write Speed", this.renderWriteSpeed()); - writeTable("Broadcast Speed", this.renderBroadcastSpeed()); - writeTable("Connect Speed", this.renderConnectSpeed()); - writeTable("Disconnect Speed", this.renderDisconnectSpeed()); - writeTable("Unlisten Speed", this.renderUnlistenSpeed()); - writeTitle("Bandwidth Report\n"); - write(BANDWIDTH_NOTE + "\n\n"); - writeTable("Downloaded Bytes", this.renderOutgoingBandwidth()); - writeTable("Uploaded Bytes", this.renderIncomingBandwidth()); - writeTable("Unindexed Queries", this.renderUnindexedData()); -}; - -ProfileReport.prototype.generateJson = function () { - return this.parse(this.processOperation.bind(this), this.outputJson.bind(this)); -}; - -ProfileReport.prototype.outputJson = function () { - var totalTime = this.state.endTime - this.state.startTime; - var tableToJson = function (table, note) { - var json = { - legend: table.options.head, - data: [], - }; - if (note) { - json.note = note; - } - table.forEach(function (row) { - // @ts-ignore - json.data.push(row); - }); - return json; - }; - var json = { - totalTime: totalTime, - readSpeed: tableToJson(this.renderReadSpeed(), SPEED_NOTE), - writeSpeed: tableToJson(this.renderWriteSpeed(), SPEED_NOTE), - broadcastSpeed: tableToJson(this.renderBroadcastSpeed(), SPEED_NOTE), - connectSpeed: tableToJson(this.renderConnectSpeed(), SPEED_NOTE), - disconnectSpeed: tableToJson(this.renderDisconnectSpeed(), SPEED_NOTE), - unlistenSpeed: tableToJson(this.renderUnlistenSpeed(), SPEED_NOTE), - downloadedBytes: tableToJson(this.renderOutgoingBandwidth(), BANDWIDTH_NOTE), - uploadedBytes: tableToJson(this.renderIncomingBandwidth(), BANDWIDTH_NOTE), - unindexedQueries: tableToJson(this.renderUnindexedData()), - }; - this.write(JSON.stringify(json, null, 2)); - if (this.options.isFile) { - return this.output.path; - } - return json; -}; - -module.exports = ProfileReport; diff --git a/src/profileReport.spec.ts b/src/profileReport.spec.ts new file mode 100644 index 00000000000..e3e3b0d7379 --- /dev/null +++ b/src/profileReport.spec.ts @@ -0,0 +1,103 @@ +import { expect } from "chai"; + +import * as stream from "stream"; +import { extractReadableIndex, formatNumber, ProfileReport } from "./profileReport"; +import { SAMPLE_INPUT_PATH, SAMPLE_OUTPUT_PATH } from "./test/fixtures/profiler-data"; + +function combinerFunc(obj1: any, obj2: any): any { + return { count: obj1.count + obj2.count }; +} + +function newReport() { + const throwAwayStream = new stream.PassThrough(); + return new ProfileReport(SAMPLE_INPUT_PATH, throwAwayStream, { + format: "JSON", + isFile: false, + collapse: true, + isInput: true, + }); +} + +describe("profilerReport", () => { + it("should correctly generate a report", () => { + const report = newReport(); + const output = require(SAMPLE_OUTPUT_PATH); + return expect(report.generate()).to.eventually.deep.equal(output); + }); + + it("should format numbers correctly", () => { + let result = formatNumber(5); + expect(result).to.eq("5"); + result = formatNumber(5.0); + expect(result).to.eq("5"); + result = formatNumber(3.33); + expect(result).to.eq("3.33"); + result = formatNumber(3.123423); + expect(result).to.eq("3.12"); + result = formatNumber(3.129); + expect(result).to.eq("3.13"); + result = formatNumber(3123423232); + expect(result).to.eq("3,123,423,232"); + result = formatNumber(3123423232.4242); + expect(result).to.eq("3,123,423,232.42"); + }); + + it("should not collapse paths if not needed", () => { + const report = newReport(); + const data: Record = {}; + for (let i = 0; i < 20; i++) { + data[`/path/num${i}`] = { count: 1 }; + } + const result = report.collapsePaths(data, combinerFunc); + expect(result).to.deep.eq(data); + }); + + it("should collapse paths to $wildcard", () => { + const report = newReport(); + const data: Record = {}; + for (let i = 0; i < 30; i++) { + data[`/path/num${i}`] = { count: 1 }; + } + const result = report.collapsePaths(data, combinerFunc); + expect(result).to.deep.eq({ "/path/$wildcard": { count: 30 } }); + }); + + it("should not collapse paths with --no-collapse", () => { + const report = newReport(); + report.options.collapse = false; + const data: Record = {}; + for (let i = 0; i < 30; i++) { + data[`/path/num${i}`] = { count: 1 }; + } + const result = report.collapsePaths(data, combinerFunc); + expect(result).to.deep.eq(data); + }); + + it("should collapse paths recursively", () => { + const report = newReport(); + const data: Record = {}; + for (let i = 0; i < 30; i++) { + data[`/path/num${i}/next${i}`] = { count: 1 }; + } + data["/path/num1/bar/test"] = { count: 1 }; + data["/foo"] = { count: 1 }; + const result = report.collapsePaths(data, combinerFunc); + expect(result).to.deep.eq({ + "/path/$wildcard/$wildcard": { count: 30 }, + "/path/$wildcard/$wildcard/test": { count: 1 }, + "/foo": { count: 1 }, + }); + }); + + it("should extract the correct path index", () => { + const query = { index: { path: ["foo", "bar"] } }; + const result = extractReadableIndex(query); + expect(result).to.eq("/foo/bar"); + }); + + it("should extract the correct value index", () => { + const query = { index: {} }; + const result = extractReadableIndex(query); + expect(result).to.eq(".value"); + }); +}); diff --git a/src/profileReport.ts b/src/profileReport.ts new file mode 100644 index 00000000000..ab0baca9dd6 --- /dev/null +++ b/src/profileReport.ts @@ -0,0 +1,661 @@ +import * as clc from "colorette"; +const Table = require("cli-table"); +import * as fs from "fs"; +import * as _ from "lodash"; +import * as readline from "readline"; + +import { FirebaseError } from "./error"; +import { logger } from "./logger"; + +const DATA_LINE_REGEX = /^data: /; + +const BANDWIDTH_NOTE = + "NOTE: The numbers reported here are only estimates of the data" + + " payloads from read operations. They are NOT a valid measure of your bandwidth bill."; + +const SPEED_NOTE = + "NOTE: Speeds are reported at millisecond resolution and" + + " are not the latencies that clients will see. Pending times" + + " are also reported at millisecond resolution. They approximate" + + " the interval of time between the instant a request is received" + + " and the instant it executes."; + +const COLLAPSE_THRESHOLD = 25; +const COLLAPSE_WILDCARD = ["$wildcard"]; + +// 'static' helper methods + +export function extractJSON(line: string, input: any): string | null { + if (!input && !DATA_LINE_REGEX.test(line)) { + return null; + } else if (!input) { + line = line.substring(5); + } + try { + return JSON.parse(line); + } catch (e) { + return null; + } +} + +export function pathString(path: string[]): string { + return `/${path ? path.join("/") : ""}`; +} + +export function formatNumber(num: number) { + const parts = num.toFixed(2).split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + if (+parts[1] === 0) { + return parts[0]; + } + return parts.join("."); +} + +export function formatBytes(bytes: number) { + const threshold = 1000; + if (Math.round(bytes) < threshold) { + return bytes + " B"; + } + const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + let u = -1; + let formattedBytes = bytes; + do { + formattedBytes /= threshold; + u++; + } while (Math.abs(formattedBytes) >= threshold && u < units.length - 1); + return formatNumber(formattedBytes) + " " + units[u]; +} + +export function extractReadableIndex(query: Record): string { + if (query.orderBy) { + return query.orderBy; + } + const indexPath: string[] = _.get(query, "index.path"); + if (indexPath) { + return pathString(indexPath); + } + return ".value"; +} + +export interface ProfileReportOptions { + format?: "JSON" | "RAW" | "TXT"; + isFile?: boolean; + collapse?: boolean; + isInput?: boolean; +} + +export class ProfileReport { + tempFile: string; + output: NodeJS.WritableStream; + options: ProfileReportOptions; + private state: any; + + constructor( + tmpFile: string, + outStream: NodeJS.WritableStream, + options: ProfileReportOptions = {}, + ) { + this.tempFile = tmpFile; + this.output = outStream; + this.options = options; + this.state = { + outband: {}, + inband: {}, + writeSpeed: {}, + broadcastSpeed: {}, + readSpeed: {}, + connectSpeed: {}, + disconnectSpeed: {}, + unlistenSpeed: {}, + unindexed: {}, + startTime: 0, + endTime: 0, + opCount: 0, + }; + } + + collectUnindexed(data: any, path: string) { + if (!data.unIndexed) { + return; + } + if (!this.state.unindexed.path) { + this.state.unindexed[path] = {}; + } + const pathNode = this.state.unindexed[path]; + // There is only ever one query. + const query = data.querySet[0]; + // Get a unique string for this query. + const index = JSON.stringify(query.index); + if (!pathNode[index]) { + pathNode[index] = { + times: 0, + query: query, + }; + } + const indexNode = pathNode[index]; + indexNode.times += 1; + } + + collectSpeedUnpathed(data: any, opStats: any) { + if (Object.keys(opStats).length === 0) { + opStats.times = 0; + opStats.millis = 0; + opStats.pendingCount = 0; + opStats.pendingTime = 0; + opStats.rejected = 0; + } + opStats.times += 1; + + if (data.hasOwnProperty("millis")) { + opStats.millis += data.millis; + } + if (data.hasOwnProperty("pendingTime")) { + opStats.pendingCount++; + opStats.pendingTime += data.pendingTime; + } + // Explictly check for false, in case its not defined. + if (data.allowed === false) { + opStats.rejected += 1; + } + } + + collectSpeed(data: any, path: string, opType: any) { + if (!opType[path]) { + opType[path] = { + times: 0, + millis: 0, + pendingCount: 0, + pendingTime: 0, + rejected: 0, + }; + } + const node = opType[path]; + node.times += 1; + /* + * If `millis` is not present, we assume that the operation is fast + * in-memory request that is not timed on the server-side (e.g. + * connects, disconnects, listens, unlistens). Such a request may + * have non-trivial `pendingTime`. + */ + if (data.hasOwnProperty("millis")) { + node.millis += data.millis; + } + if (data.hasOwnProperty("pendingTime")) { + node.pendingCount++; + node.pendingTime += data.pendingTime; + } + // Explictly check for false, in case its not defined. + if (data.allowed === false) { + node.rejected += 1; + } + } + + collectBandwidth(bytes: number, path: string, direction: any) { + if (!direction[path]) { + direction[path] = { + times: 0, + bytes: 0, + }; + } + const node = direction[path]; + node.times += 1; + node.bytes += bytes; + } + + collectRead(data: any, path: string, bytes: number) { + this.collectSpeed(data, path, this.state.readSpeed); + this.collectBandwidth(bytes, path, this.state.outband); + } + + collectBroadcast(data: any, path: string, bytes: number) { + this.collectSpeed(data, path, this.state.broadcastSpeed); + this.collectBandwidth(bytes, path, this.state.outband); + } + + collectUnlisten(data: any, path: string) { + this.collectSpeed(data, path, this.state.unlistenSpeed); + } + + collectConnect(data: any) { + this.collectSpeedUnpathed(data, this.state.connectSpeed); + } + + collectDisconnect(data: any) { + this.collectSpeedUnpathed(data, this.state.disconnectSpeed); + } + + collectWrite(data: any, path: string, bytes: number) { + this.collectSpeed(data, path, this.state.writeSpeed); + this.collectBandwidth(bytes, path, this.state.inband); + } + + processOperation(data: any) { + if (!this.state.startTime) { + this.state.startTime = data.timestamp; + } + this.state.endTime = data.timestamp; + const path = pathString(data.path); + this.state.opCount++; + switch (data.name) { + case "concurrent-connect": + this.collectConnect(data); + break; + case "concurrent-disconnect": + this.collectDisconnect(data); + break; + case "realtime-read": + this.collectRead(data, path, data.bytes); + break; + case "realtime-write": + this.collectWrite(data, path, data.bytes); + break; + case "realtime-transaction": + this.collectWrite(data, path, data.bytes); + break; + case "realtime-update": + this.collectWrite(data, path, data.bytes); + break; + case "listener-listen": + this.collectRead(data, path, data.bytes); + this.collectUnindexed(data, path); + break; + case "listener-broadcast": + this.collectBroadcast(data, path, data.bytes); + break; + case "listener-unlisten": + this.collectUnlisten(data, path); + break; + case "rest-read": + this.collectRead(data, path, data.bytes); + break; + case "rest-write": + this.collectWrite(data, path, data.bytes); + break; + case "rest-update": + this.collectWrite(data, path, data.bytes); + break; + default: + break; + } + } + + /** + * Takes an object with keys that are paths and combines the + * keys that have similar prefixes. + * Combining is done via the combiner function. + */ + collapsePaths(pathedObject: any, combiner: any, pathIndex = 1): any { + if (!this.options.collapse) { + // Don't do this if the --no-collapse flag is specified + return pathedObject; + } + const allSegments = Object.keys(pathedObject).map((path) => { + return path.split("/").filter((s) => { + return s !== ""; + }); + }); + const pathSegments = allSegments.filter((segments) => { + return segments.length > pathIndex; + }); + const otherSegments = allSegments.filter((segments) => { + return segments.length <= pathIndex; + }); + if (pathSegments.length === 0) { + return pathedObject; + } + const prefixes: Record = {}; + // Count path prefixes for the index. + pathSegments.forEach((segments) => { + const prefixPath = pathString(segments.slice(0, pathIndex)); + const prefixCount = _.get(prefixes, prefixPath, new Set()); + prefixes[prefixPath] = prefixCount.add(segments[pathIndex]); + }); + const collapsedObject: Record = {}; + pathSegments.forEach((segments) => { + const prefix = segments.slice(0, pathIndex); + const prefixPath = pathString(prefix); + const prefixCount = _.get(prefixes, prefixPath); + const originalPath = pathString(segments); + if (prefixCount.size >= COLLAPSE_THRESHOLD) { + const tail = segments.slice(pathIndex + 1); + const collapsedPath = pathString(prefix.concat(COLLAPSE_WILDCARD).concat(tail)); + const currentValue = collapsedObject[collapsedPath]; + if (currentValue) { + collapsedObject[collapsedPath] = combiner(currentValue, pathedObject[originalPath]); + } else { + collapsedObject[collapsedPath] = pathedObject[originalPath]; + } + } else { + collapsedObject[originalPath] = pathedObject[originalPath]; + } + }); + otherSegments.forEach((segments) => { + const originalPath = pathString(segments); + collapsedObject[originalPath] = pathedObject[originalPath]; + }); + // Do this again, but down a level. + return this.collapsePaths(collapsedObject, combiner, pathIndex + 1); + } + + renderUnindexedData() { + const table = new Table({ + head: ["Path", "Index", "Count"], + style: { + head: this.options.isFile ? [] : ["yellow"], + border: this.options.isFile ? [] : ["grey"], + }, + }); + const unindexed = this.collapsePaths(this.state.unindexed, (u1: any, u2: any) => { + _.mergeWith(u1, u2, (p1, p2) => { + return { + times: p1.times + p2.times, + query: p1.query, + }; + }); + }); + const paths = Object.keys(unindexed); + for (const path of paths) { + const indices = Object.keys(unindexed[path]); + for (const index of indices) { + const data = unindexed[path][index]; + const row = [path, extractReadableIndex(data.query), formatNumber(data.times)]; + table.push(row); + } + } + return table; + } + + renderBandwidth(pureData: any) { + const table = new Table({ + head: ["Path", "Total", "Count", "Average"], + style: { + head: this.options.isFile ? [] : ["yellow"], + border: this.options.isFile ? [] : ["grey"], + }, + }); + const data = this.collapsePaths(pureData, (b1: any, b2: any) => { + return { + bytes: b1.bytes + b2.bytes, + times: b1.times + b2.times, + }; + }); + const paths = Object.keys(data).sort((a: string, b: string) => { + return data[b].bytes - data[a].bytes; + }); + for (const path of paths) { + const bandwidth = data[path]; + const row = [ + path, + formatBytes(bandwidth.bytes), + formatNumber(bandwidth.times), + formatBytes(bandwidth.bytes / bandwidth.times), + ]; + table.push(row); + } + return table; + } + + renderOutgoingBandwidth() { + return this.renderBandwidth(this.state.outband); + } + + renderIncomingBandwidth() { + return this.renderBandwidth(this.state.inband); + } + + /* + * Some Realtime Database operations (concurrent-connect, concurrent-disconnect) + * are not logically associated with a path in the database. In this source + * file, we associate these operations with the sentinel path "null" so that + * they can still be aggregated in `collapsePaths`. So as to not confuse + * developers, we render aggregate statistics for such operations without a + * `path` table column. + */ + renderUnpathedOperationSpeed(speedData: any, hasSecurity = false) { + const head = ["Count", "Average Execution Speed", "Average Pending Time"]; + if (hasSecurity) { + head.push("Permission Denied"); + } + const table = new Table({ + head: head, + style: { + head: this.options.isFile ? [] : ["yellow"], + border: this.options.isFile ? [] : ["grey"], + }, + }); + /* + * If no unpathed opeartion was seen, the corresponding stats sub-object will + * be empty. + */ + if (Object.keys(speedData).length > 0) { + const row = [ + speedData.times, + formatNumber(speedData.millis / speedData.times) + " ms", + formatNumber( + speedData.pendingCount === 0 ? 0 : speedData.pendingTime / speedData.pendingCount, + ) + " ms", + ]; + if (hasSecurity) { + row.push(formatNumber(speedData.rejected)); + } + table.push(row); + } + return table; + } + + renderOperationSpeed(pureData: any, hasSecurity = false) { + const head = ["Path", "Count", "Average Execution Speed", "Average Pending Time"]; + if (hasSecurity) { + head.push("Permission Denied"); + } + const table = new Table({ + head: head, + style: { + head: this.options.isFile ? [] : ["yellow"], + border: this.options.isFile ? [] : ["grey"], + }, + }); + const data = this.collapsePaths(pureData, (s1: any, s2: any) => { + return { + times: s1.times + s2.times, + millis: s1.millis + s2.millis, + pendingCount: s1.pendingCount + s2.pendingCount, + pendingTime: s1.pendingTime + s2.pendingTime, + rejected: s1.rejected + s2.rejected, + }; + }); + const paths = Object.keys(data).sort((a, b) => { + const speedA = data[a].millis / data[a].times; + const speedB = data[b].millis / data[b].times; + return speedB - speedA; + }); + for (const path of paths) { + const speed = data[path]; + const row = [ + path, + speed.times, + formatNumber(speed.millis / speed.times) + " ms", + formatNumber(speed.pendingCount === 0 ? 0 : speed.pendingTime / speed.pendingCount) + " ms", + ]; + if (hasSecurity) { + row.push(formatNumber(speed.rejected)); + } + table.push(row); + } + return table; + } + + renderReadSpeed() { + return this.renderOperationSpeed(this.state.readSpeed, true); + } + + renderWriteSpeed() { + return this.renderOperationSpeed(this.state.writeSpeed, true); + } + + renderBroadcastSpeed() { + return this.renderOperationSpeed(this.state.broadcastSpeed, false); + } + + renderConnectSpeed() { + return this.renderUnpathedOperationSpeed(this.state.connectSpeed, false); + } + + renderDisconnectSpeed() { + return this.renderUnpathedOperationSpeed(this.state.disconnectSpeed, false); + } + + renderUnlistenSpeed() { + return this.renderOperationSpeed(this.state.unlistenSpeed, false); + } + + async parse(onLine: any, onClose: any): Promise { + const isFile = this.options.isFile; + const tmpFile = this.tempFile; + const outStream = this.output; + const isInput = this.options.isInput; + return new Promise((resolve, reject) => { + const rl = readline.createInterface({ + input: fs.createReadStream(tmpFile), + }); + let errored = false; + rl.on("line", (line) => { + const data = extractJSON(line, isInput); + if (!data) { + return; + } + onLine(data); + }); + rl.on("close", () => { + if (errored) { + reject(new FirebaseError("There was an error creating the report.")); + } else { + const result = onClose(); + if (isFile) { + // Only resolve once the data is flushed. + outStream.on("finish", () => { + resolve(result); + }); + outStream.end(); + } else { + resolve(result); + } + } + }); + rl.on("error", () => { + reject(); + }); + outStream.on("error", () => { + errored = true; + rl.close(); + }); + }); + } + + write(data: any) { + if (this.options.isFile) { + this.output.write(data); + } else { + logger.info(data); + } + } + + generate() { + if (this.options.format === "TXT") { + return this.generateText(); + } else if (this.options.format === "RAW") { + return this.generateRaw(); + } else if (this.options.format === "JSON") { + return this.generateJson(); + } + throw new FirebaseError('Invalid report format expected "TXT", "JSON", or "RAW"'); + } + + generateRaw() { + return this.parse(this.writeRaw.bind(this), () => { + return null; + }); + } + + writeRaw(data: any) { + // Just write the json to the output + this.write(JSON.stringify(data) + "\n"); + } + + generateText() { + return this.parse(this.processOperation.bind(this), this.outputText.bind(this)); + } + + outputText() { + const totalTime = this.state.endTime - this.state.startTime; + const isFile = this.options.isFile; + const write = this.write.bind(this); + const writeTitle = (title: string) => { + if (isFile) { + write(title + "\n"); + } else { + write(clc.bold(clc.yellow(title)) + "\n"); + } + }; + const writeTable = (title: string, table: typeof Table) => { + writeTitle(title); + write(table.toString() + "\n"); + }; + writeTitle( + `Report operations collected from ${new Date( + this.state.startTime, + ).toISOString()} over ${totalTime} ms.`, + ); + writeTitle("Speed Report\n"); + write(SPEED_NOTE + "\n\n"); + writeTable("Read Speed", this.renderReadSpeed()); + writeTable("Write Speed", this.renderWriteSpeed()); + writeTable("Broadcast Speed", this.renderBroadcastSpeed()); + writeTable("Connect Speed", this.renderConnectSpeed()); + writeTable("Disconnect Speed", this.renderDisconnectSpeed()); + writeTable("Unlisten Speed", this.renderUnlistenSpeed()); + writeTitle("Bandwidth Report\n"); + write(BANDWIDTH_NOTE + "\n\n"); + writeTable("Downloaded Bytes", this.renderOutgoingBandwidth()); + writeTable("Uploaded Bytes", this.renderIncomingBandwidth()); + writeTable("Unindexed Queries", this.renderUnindexedData()); + } + + generateJson() { + return this.parse(this.processOperation.bind(this), this.outputJson.bind(this)); + } + + outputJson() { + const totalTime = this.state.endTime - this.state.startTime; + const tableToJson = (table: any, note?: string) => { + const json: { legend: any; data: any[]; note?: string } = { + legend: table.options.head, + data: [], + }; + if (note) { + json.note = note; + } + table.forEach((row: any) => { + json.data.push(row); + }); + return json; + }; + const json = { + totalTime: totalTime, + readSpeed: tableToJson(this.renderReadSpeed(), SPEED_NOTE), + writeSpeed: tableToJson(this.renderWriteSpeed(), SPEED_NOTE), + broadcastSpeed: tableToJson(this.renderBroadcastSpeed(), SPEED_NOTE), + connectSpeed: tableToJson(this.renderConnectSpeed(), SPEED_NOTE), + disconnectSpeed: tableToJson(this.renderDisconnectSpeed(), SPEED_NOTE), + unlistenSpeed: tableToJson(this.renderUnlistenSpeed(), SPEED_NOTE), + downloadedBytes: tableToJson(this.renderOutgoingBandwidth(), BANDWIDTH_NOTE), + uploadedBytes: tableToJson(this.renderIncomingBandwidth(), BANDWIDTH_NOTE), + unindexedQueries: tableToJson(this.renderUnindexedData()), + }; + this.write(JSON.stringify(json, null, 2)); + if (this.options.isFile) { + return (this.output as any).path; + } + return json; + } +} diff --git a/src/profiler.ts b/src/profiler.ts index 8bad6b1efae..a7df6d28568 100644 --- a/src/profiler.ts +++ b/src/profiler.ts @@ -7,8 +7,8 @@ import AbortController from "abort-controller"; import { Client } from "./apiv2"; import { realtimeOriginOrEmulatorOrCustomUrl } from "./database/api"; import { logger } from "./logger"; -import * as ProfileReport from "./profileReport"; -import * as responseToError from "./responseToError"; +import { ProfileReport, ProfileReportOptions } from "./profileReport"; +import { responseToError } from "./responseToError"; import * as utils from "./utils"; tmp.setGracefulCleanup(); @@ -42,7 +42,7 @@ export async function profiler(options: any): Promise { spinner.stop(); controller.abort(); const dataFile = options.input || tmpFile; - const reportOptions = { + const reportOptions: ProfileReportOptions = { format: outputFormat, isFile: fileOut, isInput: !!options.input, diff --git a/src/projectPath.ts b/src/projectPath.ts index 74f660ea019..f743bca3073 100644 --- a/src/projectPath.ts +++ b/src/projectPath.ts @@ -10,7 +10,7 @@ import { FirebaseError } from "./error"; */ export function resolveProjectPath( options: { cwd?: string; configPath?: string }, - filePath: string + filePath: string, ): string { const projectRoot = detectProjectRoot(options); if (!projectRoot) { diff --git a/src/test/projectUtils.spec.ts b/src/projectUtils.spec.ts similarity index 95% rename from src/test/projectUtils.spec.ts rename to src/projectUtils.spec.ts index 187e34f9f74..94b596b5b38 100644 --- a/src/test/projectUtils.spec.ts +++ b/src/projectUtils.spec.ts @@ -1,9 +1,9 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import { needProjectNumber, needProjectId, getAliases, getProjectId } from "../projectUtils"; -import * as projects from "../management/projects"; -import { RC } from "../rc"; +import { needProjectNumber, needProjectId, getAliases, getProjectId } from "./projectUtils"; +import * as projects from "./management/projects"; +import { RC } from "./rc"; describe("getProjectId", () => { it("should prefer projectId, falling back to project", () => { @@ -65,7 +65,7 @@ describe("needProjectNumber", () => { await expect(needProjectNumber({ project: "foo" })).to.eventually.be.rejectedWith( Error, - "oh no" + "oh no", ); }); }); diff --git a/src/projectUtils.ts b/src/projectUtils.ts index f6973164e9f..588184ebb1e 100644 --- a/src/projectUtils.ts +++ b/src/projectUtils.ts @@ -1,8 +1,8 @@ import { getFirebaseProject } from "./management/projects"; import { RC } from "./rc"; -import * as clc from "cli-color"; -import * as marked from "marked"; +import * as clc from "colorette"; +import { marked } from "marked"; const { FirebaseError } = require("./error"); @@ -57,8 +57,8 @@ export function needProjectId({ clc.bold("firebase projects:list") + ".\n" + marked( - "To learn about active projects for the CLI, visit https://firebase.google.com/docs/cli#project_aliases" - ) + "To learn about active projects for the CLI, visit https://firebase.google.com/docs/cli#project_aliases", + ), ); } @@ -70,7 +70,7 @@ export function needProjectId({ "No project active, but project aliases are available.\n\nRun " + clc.bold("firebase use ") + " with one of these options:\n\n" + - aliasList + aliasList, ); } diff --git a/src/test/prompt.spec.ts b/src/prompt.spec.ts similarity index 84% rename from src/test/prompt.spec.ts rename to src/prompt.spec.ts index 0adce547099..b2ca6ca9a99 100644 --- a/src/test/prompt.spec.ts +++ b/src/prompt.spec.ts @@ -2,13 +2,14 @@ import { expect } from "chai"; import * as sinon from "sinon"; import * as inquirer from "inquirer"; -import { FirebaseError } from "../error"; -import * as prompt from "../prompt"; +import { FirebaseError } from "./error"; +import * as prompt from "./prompt"; describe("prompt", () => { let inquirerStub: sinon.SinonStub; const PROMPT_RESPONSES = { lint: true, + "lint/dint/mint": true, project: "the-best-project-ever", }; @@ -28,7 +29,7 @@ describe("prompt", () => { await expect(prompt.prompt(o, qs)).to.be.rejectedWith( FirebaseError, - /required.+non-interactive/ + /required.+non-interactive/, ); }); @@ -73,5 +74,12 @@ describe("prompt", () => { expect(r).to.equal(true); expect(inquirerStub).calledOnce; }); + + it("should handle names with .'s", async () => { + const r = await prompt.promptOnce({ name: "lint.dint.mint" }); + + expect(r).to.equal(true); + expect(inquirerStub).calledOnce; + }); }); }); diff --git a/src/prompt.ts b/src/prompt.ts index 8cd0c313c3d..a6b18e59d17 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -1,20 +1,30 @@ import * as inquirer from "inquirer"; -import * as _ from "lodash"; +import AutocompletePrompt from "inquirer-autocomplete-prompt"; import { FirebaseError } from "./error"; +declare module "inquirer" { + interface QuestionMap { + autocomplete: AutocompletePrompt.AutocompleteQuestionOptions; + } +} + +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-argument +inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt")); + /** * Question type for inquirer. See * https://www.npmjs.com/package/inquirer#question */ -export type Question = inquirer.Question; +export type Question = inquirer.DistinctQuestion; -type QuestionsThatReturnAString = +type QuestionsThatReturnAString = | inquirer.RawListQuestion | inquirer.ExpandQuestion | inquirer.InputQuestion | inquirer.PasswordQuestion - | inquirer.EditorQuestion; + | inquirer.EditorQuestion + | AutocompletePrompt.AutocompleteQuestionOptions; type Options = Record & { nonInteractive?: boolean }; @@ -22,7 +32,7 @@ type Options = Record & { nonInteractive?: boolean }; * prompt is used to prompt the user for values. Specifically, any `name` of a * provided question will be checked against the `options` object. If `name` * exists as a key in `options`, it will *not* be prompted for. If `options` - * contatins `nonInteractive = true`, then any `question.name` that does not + * contains `nonInteractive = true`, then any `question.name` that does not * have a value in `options` will cause an error to be returned. Once the values * are queried, the values for them are put onto the `options` object, and the * answers are returned. @@ -30,8 +40,15 @@ type Options = Record & { nonInteractive?: boolean }; * @param questions `Question`s to ask the user. * @return The answers, keyed by the `name` of the `Question`. */ -export async function prompt(options: Options, questions: Question[]): Promise { +export async function prompt( + options: Options, + // NB: If Observables are to be added here, the for loop below will need to + // be adjusted as well. + questions: ReadonlyArray, +): Promise { const prompts = []; + // For each of our questions, if Options already has an answer, + // we go ahead and _skip_ that question. for (const question of questions) { if (question.name && options[question.name] === undefined) { prompts.push(question); @@ -39,13 +56,12 @@ export async function prompt(options: Options, questions: Question[]): Promise p.name))).join(", "); throw new FirebaseError( `Missing required options (${missingOptions}) while running in non-interactive mode`, { children: prompts, - exit: 1, - } + }, ); } @@ -58,26 +74,26 @@ export async function prompt(options: Options, questions: Question[]): Promise( question: QuestionsThatReturnAString, - options?: Options + options?: Options, ): Promise; export async function promptOnce( question: inquirer.CheckboxQuestion, - options?: Options + options?: Options, ): Promise; export async function promptOnce( question: inquirer.ConfirmQuestion, - options?: Options + options?: Options, ): Promise; export async function promptOnce( question: inquirer.NumberQuestion, - options?: Options + options?: Options, ): Promise; -// This one is a bit hard to type out. Choices can be many things, including a genrator function. Even if we decided to limit +// This one is a bit hard to type out. Choices can be many things, including a generator function. Even if we decided to limit // the ListQuestion to have a choices of ReadonlyArray>, a ChoiceOption still has a `.value` of `any` export async function promptOnce( question: inquirer.ListQuestion, - options?: Options + options?: Options, ): Promise; /** @@ -85,8 +101,33 @@ export async function promptOnce( * @param question The question (of life, the universe, and everything). * @return The value as returned by `inquirer` for that quesiton. */ -export async function promptOnce(question: Question, options: Options = {}): Promise { - question.name = question.name || "question"; +export async function promptOnce(question: Question, options: Options = {}): Promise { + // Need to replace any .'s in the question name - otherwise, Inquirer puts the answer + // in a nested object like so: `"a.b.c" => {a: {b: {c: "my-answer"}}}` + question.name = question.name?.replace(/\./g, "/") || "question"; await prompt(options, [question]); return options[question.name]; } + +/** + * Confirm if the user wants to continue + */ +export async function confirm(args: { + nonInteractive?: boolean; + force?: boolean; + default?: boolean; + message?: string; +}): Promise { + if (!args.nonInteractive && !args.force) { + const message = args.message ?? `Do you wish to continue?`; + return await promptOnce({ + type: "confirm", + message, + default: args.default, + }); + } else if (args.nonInteractive && !args.force) { + throw new FirebaseError("Pass the --force flag to use this command in non-interactive mode"); + } else { + return true; + } +} diff --git a/src/test/rc.spec.ts b/src/rc.spec.ts similarity index 93% rename from src/test/rc.spec.ts rename to src/rc.spec.ts index 0a11387355a..f423f422a33 100644 --- a/src/test/rc.spec.ts +++ b/src/rc.spec.ts @@ -1,15 +1,14 @@ import { expect } from "chai"; import * as path from "path"; -import { RC, loadRC, RCData } from "../rc"; +import { RC, loadRC, RCData } from "./rc"; +import { CONFLICT_RC_DIR, FIREBASE_JSON_PATH, INVALID_RC_DIR } from "./test/fixtures/fbrc"; -const fixturesDir = path.resolve(__dirname, "./fixtures"); - -const EMPTY_DATA: RCData = { projects: {}, targets: {} }; +const EMPTY_DATA: RCData = { projects: {}, targets: {}, etags: {}, dataconnectEmulatorConfig: {} }; describe("RC", () => { describe(".load", () => { it("should load from nearest project directory", () => { - const result = loadRC({ cwd: path.resolve(fixturesDir, "fbrc/conflict") }); + const result = loadRC({ cwd: CONFLICT_RC_DIR }); expect(result.projects.default).to.eq("top"); }); @@ -19,12 +18,13 @@ describe("RC", () => { }); it("should not throw up on invalid json", () => { - const result = loadRC({ cwd: path.resolve(fixturesDir, "fbrc/invalid") }); + const result = loadRC({ cwd: INVALID_RC_DIR }); return expect(result.data).to.deep.eq(EMPTY_DATA); }); it("should load from the right directory when --config is specified", () => { - const result = loadRC({ cwd: __dirname, configPath: "./fixtures/fbrc/firebase.json" }); + const cwd = __dirname; + const result = loadRC({ cwd, configPath: path.relative(cwd, FIREBASE_JSON_PATH) }); expect(result.projects.default).to.eq("top"); }); }); diff --git a/src/rc.ts b/src/rc.ts index 6f0c66a79bc..111e4b28745 100644 --- a/src/rc.ts +++ b/src/rc.ts @@ -1,5 +1,5 @@ import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as cjson from "cjson"; import * as fs from "fs"; import * as path from "path"; @@ -23,6 +23,8 @@ export function loadRC(options: { cwd?: string; [other: string]: any }) { return RC.loadFile(potential); } +type EtagResourceType = "extensionInstances"; + export interface RCData { projects: { [alias: string]: string }; targets: { @@ -32,6 +34,14 @@ export interface RCData { }; }; }; + etags: { + [projectId: string]: Record>; + }; + dataconnectEmulatorConfig: { + postgres?: { + localConnectionString: string; + }; + }; } export class RC { @@ -43,7 +53,7 @@ export class RC { if (fsutils.fileExistsSync(rcpath)) { try { data = cjson.load(rcpath); - } catch (e) { + } catch (e: any) { // malformed rc file is a warning, not an error utils.logWarning("JSON error trying to load " + clc.bold(rcpath)); } @@ -53,7 +63,7 @@ export class RC { constructor(rcpath?: string, data?: Partial) { this.path = rcpath; - this.data = { projects: {}, targets: {}, ...data }; + this.data = { projects: {}, targets: {}, etags: {}, dataconnectEmulatorConfig: {}, ...data }; } private set(key: string | string[], value: any): void { @@ -113,8 +123,8 @@ export class RC { if (!TARGET_TYPES[type]) { throw new FirebaseError( `Unrecognized target type ${clc.bold(type)}. Must be one of ${Object.keys( - TARGET_TYPES - ).join(", ")}` + TARGET_TYPES, + ).join(", ")}`, ); } @@ -135,7 +145,7 @@ export class RC { // apply resources to new target const existing = this.target(project, type, targetName); - const list = _.uniq(existing.concat(resources)).sort(); + const list = Array.from(new Set(existing.concat(resources))).sort(); this.set(["targets", project, type, targetName], list); this.save(); @@ -202,21 +212,38 @@ export class RC { const target = this.target(project, type, name); if (!target.length) { throw new FirebaseError( - "Deploy target " + - clc.bold(name) + - " not configured for project " + - clc.bold(project) + - ". Configure with:\n\n firebase target:apply " + - type + - " " + - name + - " " + `Deploy target ${clc.bold(name)} not configured for project ${clc.bold( + project, + )}. Configure with: + + firebase target:apply ${type} ${name} `, ); } return target; } + getEtags(projectId: string): Record> { + return this.data.etags[projectId] || { extensionInstances: {} }; + } + + setEtags(projectId: string, resourceType: EtagResourceType, etagData: Record) { + if (!this.data.etags[projectId]) { + this.data.etags[projectId] = {} as Record>; + } + this.data.etags[projectId][resourceType] = etagData; + this.save(); + } + + getDataconnect() { + return this.data.dataconnectEmulatorConfig ?? {}; + } + + setDataconnect(localConnectionString: string) { + this.data.dataconnectEmulatorConfig = { postgres: { localConnectionString } }; + this.save(); + } + /** * Persists the RC file to disk, or returns false if no path on the instance. */ diff --git a/src/remoteconfig/get.spec.ts b/src/remoteconfig/get.spec.ts new file mode 100644 index 00000000000..f1364acc12a --- /dev/null +++ b/src/remoteconfig/get.spec.ts @@ -0,0 +1,146 @@ +import { expect } from "chai"; +import { remoteConfigApiOrigin } from "../api"; +import * as nock from "nock"; + +import * as remoteconfig from "./get"; +import { RemoteConfigTemplate } from "./interfaces"; +import { FirebaseError } from "../error"; + +const PROJECT_ID = "the-remoteconfig-test-project"; + +// Test sample template +const expectedProjectInfo: RemoteConfigTemplate = { + conditions: [ + { + name: "RCTestCondition", + expression: "dateTime < dateTime('2020-07-24T00:00:00', 'America/Los_Angeles')", + }, + ], + parameters: { + RCTestkey: { + defaultValue: { + value: "RCTestValue", + }, + }, + }, + version: { + versionNumber: "6", + updateTime: "2020-07-23T17:13:11.190Z", + updateUser: { + email: "abc@gmail.com", + }, + updateOrigin: "CONSOLE", + updateType: "INCREMENTAL_UPDATE", + }, + parameterGroups: { + RCTestCaseGroup: { + parameters: { + RCTestKey2: { + defaultValue: { + value: "RCTestValue2", + }, + description: "This is a test", + }, + }, + }, + }, + etag: "123", +}; + +// Test sample template with two parameters +const projectInfoWithTwoParameters: RemoteConfigTemplate = { + conditions: [ + { + name: "RCTestCondition", + expression: "dateTime < dateTime('2020-07-24T00:00:00', 'America/Los_Angeles')", + }, + ], + parameters: { + RCTestkey: { + defaultValue: { + value: "RCTestValue", + }, + }, + enterNumber: { + defaultValue: { + value: "6", + }, + }, + }, + version: { + versionNumber: "6", + updateTime: "2020-07-23T17:13:11.190Z", + updateUser: { + email: "abc@gmail.com", + }, + updateOrigin: "CONSOLE", + updateType: "INCREMENTAL_UPDATE", + }, + parameterGroups: { + RCTestCaseGroup: { + parameters: { + RCTestKey2: { + defaultValue: { + value: "RCTestValue2", + }, + description: "This is a test", + }, + }, + }, + }, + etag: "123", +}; + +describe("Remote Config GET", () => { + describe("getTemplate", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + it("should return the latest template", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/remoteConfig`) + .reply(200, expectedProjectInfo); + + const RCtemplate = await remoteconfig.getTemplate(PROJECT_ID); + + expect(RCtemplate).to.deep.equal(expectedProjectInfo); + }); + + it("should return the correct version of the template if version is specified", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/remoteConfig?versionNumber=${6}`) + .reply(200, expectedProjectInfo); + + const RCtemplateVersion = await remoteconfig.getTemplate(PROJECT_ID, "6"); + + expect(RCtemplateVersion).to.deep.equal(expectedProjectInfo); + }); + + it("should return a correctly parsed entry value with one parameter", () => { + const expectRCParameters = "RCTestkey\n"; + const RCParameters = remoteconfig.parseTemplateForTable(expectedProjectInfo.parameters); + + expect(RCParameters).to.deep.equal(expectRCParameters); + }); + + it("should return a correctly parsed entry value with two parameters", () => { + const expectRCParameters = "RCTestkey\nenterNumber\n"; + const RCParameters = remoteconfig.parseTemplateForTable( + projectInfoWithTwoParameters.parameters, + ); + + expect(RCParameters).to.deep.equal(expectRCParameters); + }); + + it("should reject if the api call fails", async () => { + nock(remoteConfigApiOrigin()).get(`/v1/projects/${PROJECT_ID}/remoteConfig`).reply(404, {}); + + await expect(remoteconfig.getTemplate(PROJECT_ID)).to.eventually.be.rejectedWith( + FirebaseError, + /Failed to get Firebase Remote Config template/, + ); + }); + }); +}); diff --git a/src/remoteconfig/get.ts b/src/remoteconfig/get.ts index eee4334a807..2f02bb33161 100644 --- a/src/remoteconfig/get.ts +++ b/src/remoteconfig/get.ts @@ -1,4 +1,5 @@ -import * as api from "../api"; +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; import { logger } from "../logger"; import { FirebaseError } from "../error"; import { RemoteConfigTemplate } from "./interfaces"; @@ -8,13 +9,18 @@ const TIMEOUT = 30000; // Creates a maximum limit of 50 names for each entry const MAX_DISPLAY_ITEMS = 50; +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + /** * Function retrieves names for parameters and parameter groups * @param templateItems Input is template.parameters or template.parameterGroups * @return {string} Parses the template and returns a formatted string that concatenates items and limits the number of items outputted that is used in the table */ export function parseTemplateForTable( - templateItems: RemoteConfigTemplate["parameters"] | RemoteConfigTemplate["parameterGroups"] + templateItems: RemoteConfigTemplate["parameters"] | RemoteConfigTemplate["parameterGroups"], ): string { let outputStr = ""; let counter = 0; @@ -39,24 +45,25 @@ export function parseTemplateForTable( */ export async function getTemplate( projectId: string, - versionNumber?: string + versionNumber?: string, ): Promise { try { - let request = `/v1/projects/${projectId}/remoteConfig`; + const params = new URLSearchParams(); if (versionNumber) { - request = request + "?versionNumber=" + versionNumber; + params.set("versionNumber", versionNumber); } - const response = await api.request("GET", request, { - auth: true, - origin: api.remoteConfigApiOrigin, + const res = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/remoteConfig`, + queryParams: params, timeout: TIMEOUT, }); - return response.body; - } catch (err) { + return res.body; + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to get Firebase Remote Config template for project ${projectId}. `, - { exit: 2, original: err } + { original: err }, ); } } diff --git a/src/remoteconfig/rollback.spec.ts b/src/remoteconfig/rollback.spec.ts new file mode 100644 index 00000000000..8be10710cc6 --- /dev/null +++ b/src/remoteconfig/rollback.spec.ts @@ -0,0 +1,85 @@ +import { expect } from "chai"; +import { remoteConfigApiOrigin } from "../api"; +import * as nock from "nock"; + +import { RemoteConfigTemplate } from "./interfaces"; +import * as remoteconfig from "./rollback"; +import { FirebaseError } from "../error"; + +const PROJECT_ID = "the-remoteconfig-test-project"; + +function createTemplate(versionNumber: string, date: string): RemoteConfigTemplate { + return { + parameterGroups: {}, + version: { + updateUser: { + email: "jackiechu@google.com", + }, + updateTime: date, + updateOrigin: "REST_API", + versionNumber: versionNumber, + }, + conditions: [], + parameters: {}, + etag: "123", + }; +} + +const latestTemplate: RemoteConfigTemplate = createTemplate("115", "2020-08-06T23:11:41.629Z"); +const rollbackTemplate: RemoteConfigTemplate = createTemplate("114", "2020-08-07T23:11:41.629Z"); + +describe("RemoteConfig Rollback", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + describe("rollbackCurrentVersion", () => { + it("should return a rollback to the version number specified", async () => { + nock(remoteConfigApiOrigin()) + .post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=${115}`) + .reply(200, latestTemplate); + + const RCtemplate = await remoteconfig.rollbackTemplate(PROJECT_ID, 115); + + expect(RCtemplate).to.deep.equal(latestTemplate); + }); + + // TODO: there is no logic that this is testing. Is that intentional? + it.skip("should reject invalid rollback version number", async () => { + nock(remoteConfigApiOrigin()) + .post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=${1000}`) + .reply(200, latestTemplate); + + const RCtemplate = await remoteconfig.rollbackTemplate(PROJECT_ID, 1000); + + expect(RCtemplate).to.deep.equal(latestTemplate); + try { + await remoteconfig.rollbackTemplate(PROJECT_ID); + } catch (e: any) { + e; + } + }); + + // TODO: this also is not testing anything in the file. Is this intentional? + it.skip("should return a rollback to the previous version", async () => { + nock(remoteConfigApiOrigin()) + .post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=${undefined}`) + .reply(200, rollbackTemplate); + + const RCtemplate = await remoteconfig.rollbackTemplate(PROJECT_ID); + + expect(RCtemplate).to.deep.equal(rollbackTemplate); + }); + + it("should reject if the api call fails", async () => { + nock(remoteConfigApiOrigin()) + .post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=${4}`) + .reply(404, {}); + await expect(remoteconfig.rollbackTemplate(PROJECT_ID, 4)).to.eventually.be.rejectedWith( + FirebaseError, + /Not Found/, + ); + }); + }); +}); diff --git a/src/remoteconfig/rollback.ts b/src/remoteconfig/rollback.ts index ec1bd27efd2..945f071d245 100644 --- a/src/remoteconfig/rollback.ts +++ b/src/remoteconfig/rollback.ts @@ -1,4 +1,11 @@ -import api = require("../api"); +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { RemoteConfigTemplate } from "./interfaces"; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); const TIMEOUT = 30000; @@ -6,14 +13,19 @@ const TIMEOUT = 30000; * Rolls back to a specific version of the Remote Config template * @param projectId Remote Config Template Project Id * @param versionNumber Remote Config Template version number to roll back to - * @return {Promise} Returns a promise of a Remote Config Template using the RemoteConfigTemplate interface + * @return Returns a promise of a Remote Config Template using the RemoteConfigTemplate interface */ -export async function rollbackTemplate(projectId: string, versionNumber?: number): Promise { - const requestPath = `/v1/projects/${projectId}/remoteConfig:rollback?versionNumber=${versionNumber}`; - const response = await api.request("POST", requestPath, { - auth: true, - origin: api.remoteConfigApiOrigin, +export async function rollbackTemplate( + projectId: string, + versionNumber?: number, +): Promise { + const params = new URLSearchParams(); + params.set("versionNumber", `${versionNumber}`); + const res = await apiClient.request({ + method: "POST", + path: `/projects/${projectId}/remoteConfig:rollback`, + queryParams: params, timeout: TIMEOUT, }); - return response.body; + return res.body; } diff --git a/src/remoteconfig/versionslist.spec.ts b/src/remoteconfig/versionslist.spec.ts new file mode 100644 index 00000000000..88e7005db60 --- /dev/null +++ b/src/remoteconfig/versionslist.spec.ts @@ -0,0 +1,105 @@ +import { expect } from "chai"; +import { remoteConfigApiOrigin } from "../api"; +import * as nock from "nock"; + +import * as remoteconfig from "./versionslist"; +import { ListVersionsResult, Version } from "./interfaces"; + +const PROJECT_ID = "the-remoteconfig-test-project"; + +function createVersion(version: string, date: string): Version { + return { + versionNumber: version, + updateTime: date, + updateUser: { email: "jackiechu@google.com" }, + }; +} +// Test template with limit of 2 +const expectedProjectInfoLimit: ListVersionsResult = { + versions: [ + createVersion("114", "2020-07-16T23:22:23.608Z"), + createVersion("113", "2020-06-18T21:10:08.992Z"), + ], +}; + +// Test template with no limit (default template) +const expectedProjectInfoDefault: ListVersionsResult = { + versions: [ + ...expectedProjectInfoLimit.versions, + createVersion("112", "2020-06-16T22:20:34.549Z"), + createVersion("111", "2020-06-16T22:14:24.419Z"), + createVersion("110", "2020-06-16T22:05:03.116Z"), + createVersion("109", "2020-06-16T21:55:19.415Z"), + createVersion("108", "2020-06-16T21:54:55.799Z"), + createVersion("107", "2020-06-16T21:48:37.565Z"), + createVersion("106", "2020-06-16T21:44:41.043Z"), + createVersion("105", "2020-06-16T21:44:13.860Z"), + ], +}; + +// Test template with limit of 0 +const expectedProjectInfoNoLimit: ListVersionsResult = { + versions: [ + ...expectedProjectInfoDefault.versions, + createVersion("104", "2020-06-16T21:39:19.422Z"), + createVersion("103", "2020-06-16T21:37:40.858Z"), + ], +}; + +describe("RemoteConfig ListVersions", () => { + describe("getVersionTemplate", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + it("should return the list of versions up to the limit", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=${2}`) + .reply(200, expectedProjectInfoLimit); + + const RCtemplate = await remoteconfig.getVersions(PROJECT_ID, 2); + + expect(RCtemplate).to.deep.equal(expectedProjectInfoLimit); + }); + + it("should return all the versions when the limit is 0", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=${300}`) + .reply(200, expectedProjectInfoNoLimit); + + const RCtemplate = await remoteconfig.getVersions(PROJECT_ID, 0); + + expect(RCtemplate).to.deep.equal(expectedProjectInfoNoLimit); + }); + + it("should return with default 10 versions when no limit is set", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=${10}`) + .reply(200, expectedProjectInfoDefault); + + const RCtemplateVersion = await remoteconfig.getVersions(PROJECT_ID); + + expect(RCtemplateVersion.versions.length).to.deep.equal(10); + expect(RCtemplateVersion).to.deep.equal(expectedProjectInfoDefault); + }); + + it("should reject if the api call fails", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=${10}`) + .reply(404, "Not Found"); + + let err; + try { + await remoteconfig.getVersions(PROJECT_ID); + } catch (e: any) { + err = e; + } + + expect(err).to.not.be.undefined; + expect(err.message).to.equal( + `Failed to get Remote Config template versions for Firebase project ${PROJECT_ID}. `, + ); + }); + }); +}); diff --git a/src/remoteconfig/versionslist.ts b/src/remoteconfig/versionslist.ts index 9f58b8baf0f..cbfc0e21a5a 100644 --- a/src/remoteconfig/versionslist.ts +++ b/src/remoteconfig/versionslist.ts @@ -1,8 +1,14 @@ -import api = require("../api"); +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; import { FirebaseError } from "../error"; import { ListVersionsResult } from "./interfaces"; import { logger } from "../logger"; +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + const TIMEOUT = 30000; /** @@ -14,21 +20,22 @@ const TIMEOUT = 30000; export async function getVersions(projectId: string, maxResults = 10): Promise { maxResults = maxResults || 300; try { - let request = `/v1/projects/${projectId}/remoteConfig:listVersions`; + const params = new URLSearchParams(); if (maxResults) { - request = request + "?pageSize=" + maxResults; + params.set("pageSize", `${maxResults}`); } - const response = await api.request("GET", request, { - auth: true, - origin: api.remoteConfigApiOrigin, + const response = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/remoteConfig:listVersions`, + queryParams: params, timeout: TIMEOUT, }); return response.body; - } catch (err) { + } catch (err: any) { logger.debug(err.message); throw new FirebaseError( `Failed to get Remote Config template versions for Firebase project ${projectId}. `, - { exit: 2, original: err } + { original: err }, ); } } diff --git a/src/requireAuth.ts b/src/requireAuth.ts index 1f15e57db04..09329fc058d 100644 --- a/src/requireAuth.ts +++ b/src/requireAuth.ts @@ -1,5 +1,5 @@ import { GoogleAuth, GoogleAuthOptions } from "google-auth-library"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as api from "./api"; import * as apiv2 from "./apiv2"; @@ -7,10 +7,12 @@ import { FirebaseError } from "./error"; import { logger } from "./logger"; import * as utils from "./utils"; import * as scopes from "./scopes"; -import { Tokens, User, setRefreshToken, setActiveAccount } from "./auth"; +import { Tokens, User } from "./types/auth"; +import { setRefreshToken, setActiveAccount } from "./auth"; +import type { Options } from "./options"; const AUTH_ERROR_MESSAGE = `Command requires authentication, please run ${clc.bold( - "firebase login" + "firebase login", )}`; let authClient: GoogleAuth | undefined; @@ -30,21 +32,35 @@ function getAuthClient(config: GoogleAuthOptions): GoogleAuth { /** * Retrieves and sets the access token for the current user. + * Returns account email if found. * @param options CLI options. * @param authScopes scopes to be obtained. */ -async function autoAuth(options: any, authScopes: string[]): Promise { +async function autoAuth(options: Options, authScopes: string[]): Promise { + if (process.env.MONOSPACE_ENV) { + throw new FirebaseError("autoAuth not yet implemented for IDX. Please run 'firebase login'"); + } const client = getAuthClient({ scopes: authScopes, projectId: options.project }); const token = await client.getAccessToken(); - api.setAccessToken(token); token !== null ? apiv2.setAccessToken(token) : false; + + let clientEmail; + try { + const credentials = await client.getCredentials(); + clientEmail = credentials.client_email; + } catch (e) { + // Make sure any error here doesn't block the CLI, but log it. + logger.debug(`Error getting account credentials.`); + } + + return clientEmail; } /** * Ensures that there is an authenticated user. * @param options CLI options. */ -export async function requireAuth(options: any): Promise { +export async function requireAuth(options: any): Promise { api.setScopes([scopes.CLOUD_PLATFORM, scopes.FIREBASE_PLATFORM]); options.authScopes = api.getScopes(); @@ -54,17 +70,25 @@ export async function requireAuth(options: any): Promise { let tokenOpt = utils.getInheritedOption(options, "token"); if (tokenOpt) { logger.debug("> authorizing via --token option"); + utils.logWarning( + "Authenticating with `--token` is deprecated and will be removed in a future major version of `firebase-tools`. " + + "Instead, use a service account key with `GOOGLE_APPLICATION_CREDENTIALS`: https://cloud.google.com/docs/authentication/getting-started", + ); } else if (process.env.FIREBASE_TOKEN) { logger.debug("> authorizing via FIREBASE_TOKEN environment variable"); + utils.logWarning( + "Authenticating with `FIREBASE_TOKEN` is deprecated and will be removed in a future major version of `firebase-tools`. " + + "Instead, use a service account key with `GOOGLE_APPLICATION_CREDENTIALS`: https://cloud.google.com/docs/authentication/getting-started", + ); } else if (user) { logger.debug(`> authorizing via signed-in user (${user.email})`); } else { try { return await autoAuth(options, options.authScopes); - } catch (e) { + } catch (e: any) { throw new FirebaseError( `Failed to authenticate, have you run ${clc.bold("firebase login")}?`, - { original: e } + { original: e }, ); } } @@ -81,4 +105,5 @@ export async function requireAuth(options: any): Promise { } setActiveAccount(options, { user, tokens }); + return user.email; } diff --git a/src/requireConfig.js b/src/requireConfig.js deleted file mode 100644 index d7eda280a7a..00000000000 --- a/src/requireConfig.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; - -var { FirebaseError } = require("./error"); - -module.exports = function (options) { - if (options.config) { - return Promise.resolve(); - } - return Promise.reject( - options.configError || - new FirebaseError("Not in a Firebase project directory (could not locate firebase.json)", { - exit: 1, - }) - ); -}; diff --git a/src/requireConfig.spec.ts b/src/requireConfig.spec.ts new file mode 100644 index 00000000000..cbdfe4dd08c --- /dev/null +++ b/src/requireConfig.spec.ts @@ -0,0 +1,47 @@ +import { expect } from "chai"; +import { Config } from "./config"; +import { FirebaseError } from "./error"; +import { Options } from "./options"; +import { RC } from "./rc"; +import { requireConfig } from "./requireConfig"; +import { cloneDeep } from "./utils"; + +const options: Options = { + cwd: "", + configPath: "", + only: "", + except: "", + config: new Config({}), + filteredTargets: [], + force: false, + json: false, + nonInteractive: false, + interactive: false, + debug: false, + rc: new RC(), +}; + +describe("requireConfig", () => { + it("should resolve if config exists", async () => { + // This returns nothing to test - it just should not throw. + await requireConfig(options); + }); + + it("should fail if config does not exist", async () => { + const o: unknown = cloneDeep(options); + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + delete (o as any).config; + await expect(requireConfig(o as Options)).to.eventually.be.rejectedWith( + FirebaseError, + /Not in a Firebase project directory/, + ); + }); + + it("should return the existing configError if one is set", async () => { + const o = cloneDeep(options); + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + delete (o as any).config; + o.configError = new Error("This is a config error."); + await expect(requireConfig(o)).to.eventually.be.rejectedWith(Error, /This is a config error./); + }); +}); diff --git a/src/requireConfig.ts b/src/requireConfig.ts new file mode 100644 index 00000000000..80719c064b9 --- /dev/null +++ b/src/requireConfig.ts @@ -0,0 +1,18 @@ +import { FirebaseError } from "./error"; +import { Options } from "./options"; + +/** + * Rejects if there is no config in `options`. + */ +export async function requireConfig(options: Options): Promise { + return new Promise((resolve, reject) => + options.config + ? resolve() + : reject( + options.configError ?? + new FirebaseError( + "Not in a Firebase project directory (could not locate firebase.json)", + ), + ), + ); +} diff --git a/src/requireDatabaseInstance.ts b/src/requireDatabaseInstance.ts index e1ed760346e..e11f56505fe 100644 --- a/src/requireDatabaseInstance.ts +++ b/src/requireDatabaseInstance.ts @@ -1,12 +1,12 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { FirebaseError } from "./error"; import { getDefaultDatabaseInstance } from "./getDefaultDatabaseInstance"; /** * Error message to be returned when the default database instance is found to be missing. */ -export const MISSING_DEFAULT_INSTANCE_ERROR_MESSAGE = `It looks like you haven't created a Realtime Database instance in this project before. Please run ${clc.bold.underline( - "firebase init database" +export const MISSING_DEFAULT_INSTANCE_ERROR_MESSAGE = `It looks like you haven't created a Realtime Database instance in this project before. Please run ${clc.bold( + clc.underline("firebase init database"), )} to create your default Realtime Database instance.`; /** @@ -21,7 +21,7 @@ export async function requireDatabaseInstance(options: any): Promise { let instance; try { instance = await getDefaultDatabaseInstance(options); - } catch (err) { + } catch (err: any) { throw new FirebaseError(`Failed to get details for project: ${options.project}.`, { original: err, }); diff --git a/src/requireInteractive.ts b/src/requireInteractive.ts index 54dcf5e8f58..5e42276036f 100644 --- a/src/requireInteractive.ts +++ b/src/requireInteractive.ts @@ -7,7 +7,7 @@ export default function requireInteractive(options: Options) { return Promise.reject( new FirebaseError("This command cannot run in non-interactive mode", { exit: 1, - }) + }), ); } return Promise.resolve(); diff --git a/src/requirePermissions.ts b/src/requirePermissions.ts index aaa9b2efc86..9389f0b3c40 100644 --- a/src/requirePermissions.ts +++ b/src/requirePermissions.ts @@ -1,5 +1,5 @@ -import { bold } from "cli-color"; -import { needProjectId } from "./projectUtils"; +import { bold } from "colorette"; +import { getProjectId } from "./projectUtils"; import { requireAuth } from "./requireAuth"; import { logger } from "./logger"; import { FirebaseError } from "./error"; @@ -16,13 +16,16 @@ const BASE_PERMISSIONS = ["firebase.projects.get"]; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function requirePermissions(options: any, permissions: string[] = []): Promise { - const projectId = needProjectId(options); + const projectId = getProjectId(options); + if (!projectId) { + return; + } const requiredPermissions = BASE_PERMISSIONS.concat(permissions).sort(); await requireAuth(options); logger.debug( - `[iam] checking project ${projectId} for permissions ${JSON.stringify(requiredPermissions)}` + `[iam] checking project ${projectId} for permissions ${JSON.stringify(requiredPermissions)}`, ); try { @@ -30,11 +33,11 @@ export async function requirePermissions(options: any, permissions: string[] = [ if (!iamResult.passed) { throw new FirebaseError( `Authorization failed. This account is missing the following required permissions on project ${bold( - projectId - )}:\n\n ${iamResult.missing.join("\n ")}` + projectId, + )}:\n\n ${iamResult.missing.join("\n ")}`, ); } - } catch (err) { + } catch (err: any) { logger.debug(`[iam] error while checking permissions, command may fail: ${err}`); return; } diff --git a/src/requireTosAcceptance.spec.ts b/src/requireTosAcceptance.spec.ts new file mode 100644 index 00000000000..736b198481c --- /dev/null +++ b/src/requireTosAcceptance.spec.ts @@ -0,0 +1,78 @@ +import * as nock from "nock"; +import { APPHOSTING_TOS_ID, APP_CHECK_TOS_ID } from "./gcp/firedata"; +import { requireTosAcceptance } from "./requireTosAcceptance"; +import { Options } from "./options"; +import { RC } from "./rc"; +import { expect } from "chai"; + +const SAMPLE_OPTIONS: Options = { + cwd: "/", + configPath: "/", + /* eslint-disable-next-line */ + config: {} as any, + only: "", + except: "", + nonInteractive: false, + json: false, + interactive: false, + debug: false, + force: false, + filteredTargets: [], + rc: new RC(), +}; + +const SAMPLE_RESPONSE = { + perServiceStatus: [ + { + tosId: "APP_CHECK", + serviceStatus: { + tos: { + id: "app_check", + tosId: "APP_CHECK", + }, + status: "ACCEPTED", + }, + }, + { + tosId: "APP_HOSTING_TOS", + serviceStatus: { + tos: { + id: "app_hosting", + tosId: "APP_HOSTING_TOS", + }, + status: "TERMS_UPDATED", + }, + }, + ], +}; + +describe("requireTosAcceptance", () => { + before(() => { + nock.disableNetConnect(); + }); + after(() => { + nock.enableNetConnect(); + }); + + it("should resolve for accepted terms of service", async () => { + nock("https://mobilesdk-pa.googleapis.com") + .get("/v1/accessmanagement/tos:getStatus") + .reply(200, SAMPLE_RESPONSE); + + await requireTosAcceptance(APP_CHECK_TOS_ID)(SAMPLE_OPTIONS); + + expect(nock.isDone()).to.be.true; + }); + + it("should throw error if not accepted", async () => { + nock("https://mobilesdk-pa.googleapis.com") + .get("/v1/accessmanagement/tos:getStatus") + .reply(200, SAMPLE_RESPONSE); + + await expect(requireTosAcceptance(APPHOSTING_TOS_ID)(SAMPLE_OPTIONS)).to.be.rejectedWith( + "Terms of Service", + ); + + expect(nock.isDone()).to.be.true; + }); +}); diff --git a/src/requireTosAcceptance.ts b/src/requireTosAcceptance.ts new file mode 100644 index 00000000000..d6a5ef3280c --- /dev/null +++ b/src/requireTosAcceptance.ts @@ -0,0 +1,42 @@ +import type { Options } from "./options"; + +import { FirebaseError } from "./error"; +import { + APPHOSTING_TOS_ID, + DATA_CONNECT_TOS_ID, + TosId, + getTosStatus, + isProductTosAccepted, +} from "./gcp/firedata"; +import { consoleOrigin } from "./api"; + +const consoleLandingPage = new Map([ + [APPHOSTING_TOS_ID, `${consoleOrigin()}/project/_/apphosting`], + [DATA_CONNECT_TOS_ID, `${consoleOrigin()}/project/_/dataconnect`], +]); + +/** + * Returns a function that checks product terms of service. Useful for Command `before` hooks. + * + * Example: + * new Command(...) + * .description(...) + * .before(requireTosAcceptance(APPHOSTING_TOS_ID)) ; + * + * Note: When supporting new products, be sure to update `consoleLandingPage` above to avoid surfacing + * generic ToS error messages. + */ +export function requireTosAcceptance(tosId: TosId): (options: Options) => Promise { + return () => requireTos(tosId); +} + +async function requireTos(tosId: TosId): Promise { + const res = await getTosStatus(); + if (isProductTosAccepted(res, tosId)) { + return; + } + const console = consoleLandingPage.get(tosId) || consoleOrigin(); + throw new FirebaseError( + `Your account has not accepted the required Terms of Service for this action. Please accept the Terms of Service and try again. ${console}`, + ); +} diff --git a/src/responseToError.js b/src/responseToError.js deleted file mode 100644 index 7ed45086e54..00000000000 --- a/src/responseToError.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; - -const _ = require("lodash"); -const { FirebaseError } = require("./error"); - -module.exports = function (response, body) { - if (typeof body === "string" && response.statusCode === 404) { - body = { - error: { - message: "Not Found", - }, - }; - } - - if (response.statusCode < 400) { - return null; - } - - if (typeof body !== "object") { - try { - body = JSON.parse(body); - } catch (e) { - body = {}; - } - } - - if (!body.error) { - const errMessage = response.statusCode === 404 ? "Not Found" : "Unknown Error"; - body.error = { - message: errMessage, - }; - } - - const message = "HTTP Error: " + response.statusCode + ", " + (body.error.message || body.error); - - let exitCode; - if (response.statusCode >= 500) { - // 5xx errors are unexpected - exitCode = 2; - } else { - // 4xx errors happen sometimes - exitCode = 1; - } - - _.unset(response, "request.headers"); - return new FirebaseError(message, { - context: { - body: body, - response: response, - }, - exit: exitCode, - status: response.statusCode, - }); -}; diff --git a/src/responseToError.ts b/src/responseToError.ts new file mode 100644 index 00000000000..4fcef63bd29 --- /dev/null +++ b/src/responseToError.ts @@ -0,0 +1,61 @@ +import * as _ from "lodash"; + +import { FirebaseError } from "./error"; + +export function responseToError(response: any, body: any): FirebaseError | undefined { + if (response.statusCode < 400) { + return; + } + + if (typeof body === "string") { + if (response.statusCode === 404) { + body = { + error: { + message: "Not Found", + }, + }; + } else { + body = { + error: { + message: body, + }, + }; + } + } + + if (typeof body !== "object") { + try { + body = JSON.parse(body); + } catch (e) { + body = {}; + } + } + + if (!body.error) { + const errMessage = response.statusCode === 404 ? "Not Found" : "Unknown Error"; + body.error = { + message: errMessage, + }; + } + + const message = "HTTP Error: " + response.statusCode + ", " + (body.error.message || body.error); + + let exitCode; + if (response.statusCode >= 500) { + // 5xx errors are unexpected + exitCode = 2; + } else { + // 4xx errors happen sometimes + exitCode = 1; + } + + _.unset(response, "request.headers"); + return new FirebaseError(message, { + context: { + body: body, + response: response, + }, + exit: exitCode, + status: response.statusCode, + }); +} diff --git a/src/rtdb.js b/src/rtdb.js deleted file mode 100644 index 00c75abcf5b..00000000000 --- a/src/rtdb.js +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; - -var api = require("./api"); -var { FirebaseError } = require("./error"); -var utils = require("./utils"); -const { populateInstanceDetails } = require("./management/database"); -const { realtimeOriginOrCustomUrl } = require("./database/api"); -exports.updateRules = function (projectId, instance, src, options) { - options = options || {}; - var path = ".settings/rules.json"; - if (options.dryRun) { - path += "?dryRun=true"; - } - var downstreamOptions = { instance: instance, project: projectId }; - return populateInstanceDetails(downstreamOptions) - .then(function () { - const origin = utils.getDatabaseUrl( - realtimeOriginOrCustomUrl(downstreamOptions.instanceDetails.databaseUrl), - instance, - "" - ); - return api.request("PUT", path, { - origin: origin, - auth: true, - data: src, - json: false, - resolveOnHTTPError: true, - }); - }) - .then(function (response) { - if (response.status === 400) { - throw new FirebaseError( - "Syntax error in database rules:\n\n" + JSON.parse(response.body).error - ); - } else if (response.status > 400) { - throw new FirebaseError("Unexpected error while deploying database rules.", { exit: 2 }); - } - }); -}; diff --git a/src/rtdb.ts b/src/rtdb.ts new file mode 100644 index 00000000000..f3407f3beea --- /dev/null +++ b/src/rtdb.ts @@ -0,0 +1,47 @@ +import { Client } from "./apiv2"; +import { DatabaseInstance, populateInstanceDetails } from "./management/database"; +import { FirebaseError } from "./error"; +import { realtimeOriginOrCustomUrl } from "./database/api"; +import * as utils from "./utils"; + +/** + * Updates rules, optionally specifying a dry run flag for validation purposes. + */ +export async function updateRules( + projectId: string, + instance: string, + src: any, + options: { dryRun?: boolean } = {}, +): Promise { + const queryParams: { dryRun?: string } = {}; + if (options.dryRun) { + queryParams.dryRun = "true"; + } + const downstreamOptions: { + instance: string; + project: string; + instanceDetails?: DatabaseInstance; + } = { instance: instance, project: projectId }; + await populateInstanceDetails(downstreamOptions); + if (!downstreamOptions.instanceDetails) { + throw new FirebaseError(`Could not get instance details`, { exit: 2 }); + } + const origin = utils.getDatabaseUrl( + realtimeOriginOrCustomUrl(downstreamOptions.instanceDetails.databaseUrl), + instance, + "", + ); + const client = new Client({ urlPrefix: origin }); + const response = await client.request({ + method: "PUT", + path: ".settings/rules.json", + queryParams, + body: src, + resolveOnHTTPError: true, + }); + if (response.status === 400) { + throw new FirebaseError(`Syntax error in database rules:\n\n${response.body.error}`); + } else if (response.status > 400) { + throw new FirebaseError("Unexpected error while deploying database rules.", { exit: 2 }); + } +} diff --git a/src/test/rulesDeploy.spec.ts b/src/rulesDeploy.spec.ts similarity index 78% rename from src/test/rulesDeploy.spec.ts rename to src/rulesDeploy.spec.ts index 25d8a2f4695..e93ec394679 100644 --- a/src/test/rulesDeploy.spec.ts +++ b/src/rulesDeploy.spec.ts @@ -1,27 +1,27 @@ import { expect } from "chai"; -import * as path from "path"; import * as sinon from "sinon"; -import { FirebaseError } from "../error"; -import * as prompt from "../prompt"; +import { FirebaseError } from "./error"; +import * as prompt from "./prompt"; +import * as resourceManager from "./gcp/resourceManager"; +import * as projectNumber from "./getProjectNumber"; import { readFileSync } from "fs-extra"; -import { RulesetFile } from "../gcp/rules"; -import { Config } from "../config"; -import gcp = require("../gcp"); +import { RulesetFile } from "./gcp/rules"; +import { Config } from "./config"; +import * as gcp from "./gcp"; -import { RulesDeploy, RulesetServiceType } from "../rulesDeploy"; +import { RulesDeploy, RulesetServiceType } from "./rulesDeploy"; +import { FIXTURE_DIR, FIXTURE_FIRESTORE_RULES_PATH } from "./test/fixtures/rulesDeploy"; +import { FIXTURE_DIR as CROSS_SERVICE_FIXTURE_DIR } from "./test/fixtures/rulesDeployCrossService"; describe("RulesDeploy", () => { - const FIXTURE_DIR = path.resolve(__dirname, "fixtures/rulesDeploy"); const BASE_OPTIONS: { cwd: string; project: string; config: any } = { cwd: FIXTURE_DIR, project: "test-project", config: null, }; BASE_OPTIONS.config = Config.load(BASE_OPTIONS, false); - const FIRESTORE_RULES_CONTENT = readFileSync( - path.resolve(FIXTURE_DIR, "firestore.rules") - ).toString(); + const FIRESTORE_RULES_CONTENT = readFileSync(FIXTURE_FIRESTORE_RULES_PATH).toString(); describe("addFile", () => { it("should successfully add a file that exists", () => { @@ -29,7 +29,7 @@ describe("RulesDeploy", () => { expect(() => { rd.addFile("firestore.rules"); - }).to.not.throw; + }).to.not.throw(); }); it("should throw an error if the file does not exist", () => { @@ -125,7 +125,7 @@ describe("RulesDeploy", () => { const result = rd.compile(); await expect(result).to.eventually.be.rejectedWith( Error, - /Compilation error in .+storage.rules.+:\n\[E\] 0:0 - oopsie/ + /Compilation error in .*storage.rules.*:\n\[E\] 0:0 - oopsie/, ); }); @@ -156,7 +156,7 @@ describe("RulesDeploy", () => { const result = rd.compile(); await expect(result).to.eventually.be.rejectedWith( Error, - /Compilation errors in .+storage.rules.+:\n\[E\] 0:0 - oopsie\n\[E\] 1:1 - daisey/ + /Compilation errors in .*storage.rules.*:\n\[E\] 0:0 - oopsie\n\[E\] 1:1 - daisey/, ); }); @@ -337,6 +337,86 @@ describe("RulesDeploy", () => { }); }); + describe("with cross-service rules", () => { + const CROSS_SERVICE_OPTIONS: { cwd: string; project: string; config: any } = { + cwd: CROSS_SERVICE_FIXTURE_DIR, + project: "test-project", + config: null, + }; + CROSS_SERVICE_OPTIONS.config = Config.load(CROSS_SERVICE_OPTIONS, false); + + beforeEach(() => { + (gcp.rules.getLatestRulesetName as sinon.SinonStub).resolves(null); + (gcp.rules.createRuleset as sinon.SinonStub).onFirstCall().resolves("compiled"); + sinon.stub(projectNumber, "getProjectNumber").resolves("12345"); + rd = new RulesDeploy(CROSS_SERVICE_OPTIONS, RulesetServiceType.FIREBASE_STORAGE); + rd.addFile("storage.rules"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should deploy even with IAM failure", async () => { + sinon.stub(resourceManager, "serviceAccountHasRoles").rejects(); + const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE); + await expect(result).to.eventually.deep.equal(["compiled"]); + + expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [ + { name: "storage.rules", content: sinon.match.string }, + ]); + expect(resourceManager.serviceAccountHasRoles).calledOnce; + }); + + it("should update permissions if prompted", async () => { + sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(false); + sinon.stub(resourceManager, "addServiceAccountToRoles").resolves(); + sinon.stub(prompt, "promptOnce").onFirstCall().resolves(true); + + const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE); + await expect(result).to.eventually.deep.equal(["compiled"]); + + expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [ + { name: "storage.rules", content: sinon.match.string }, + ]); + expect(resourceManager.addServiceAccountToRoles).calledOnceWithExactly( + "12345", + "service-12345@gcp-sa-firebasestorage.iam.gserviceaccount.com", + ["roles/firebaserules.firestoreServiceAgent"], + true, + ); + }); + + it("should not update permissions if declined", async () => { + sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(false); + sinon.stub(resourceManager, "addServiceAccountToRoles").resolves(); + sinon.stub(prompt, "promptOnce").onFirstCall().resolves(false); + + const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE); + await expect(result).to.eventually.deep.equal(["compiled"]); + + expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [ + { name: "storage.rules", content: sinon.match.string }, + ]); + expect(resourceManager.addServiceAccountToRoles).not.called; + }); + + it("should not prompt if role already granted", async () => { + sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(true); + sinon.stub(resourceManager, "addServiceAccountToRoles").resolves(); + const promptSpy = sinon.spy(prompt, "promptOnce"); + + const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE); + await expect(result).to.eventually.deep.equal(["compiled"]); + + expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [ + { name: "storage.rules", content: sinon.match.string }, + ]); + expect(resourceManager.addServiceAccountToRoles).not.called; + expect(promptSpy).not.called; + }); + }); + describe("when there are quota issues", () => { const QUOTA_ERROR = new Error("quota error"); (QUOTA_ERROR as any).status = 429; @@ -396,7 +476,7 @@ describe("RulesDeploy", () => { it("should prompt for a choice (yes) and delete and retry creation", async () => { (gcp.rules.createRuleset as sinon.SinonStub).onFirstCall().rejects(QUOTA_ERROR); (gcp.rules.listAllRulesets as sinon.SinonStub).resolves( - new Array(1001).fill(0).map(() => ({ name: "foo" })) + new Array(1001).fill(0).map(() => ({ name: "foo" })), ); (prompt.promptOnce as sinon.SinonStub).onFirstCall().resolves(true); (gcp.rules.listAllReleases as sinon.SinonStub).resolves([ @@ -438,7 +518,7 @@ describe("RulesDeploy", () => { expect(gcp.rules.updateOrCreateRelease).calledOnceWithExactly( BASE_OPTIONS.project, undefined, // Because we didn't compile anything. - RulesetServiceType.CLOUD_FIRESTORE + RulesetServiceType.CLOUD_FIRESTORE, ); }); @@ -446,7 +526,7 @@ describe("RulesDeploy", () => { const result = rd.release("firestore.rules", RulesetServiceType.FIREBASE_STORAGE); await expect(result).to.eventually.be.rejectedWith( FirebaseError, - /Cannot release resource type "firebase.storage"/ + /Cannot release resource type "firebase.storage"/, ); expect(gcp.rules.updateOrCreateRelease).not.called; @@ -461,7 +541,7 @@ describe("RulesDeploy", () => { expect(gcp.rules.updateOrCreateRelease).calledOnceWithExactly( BASE_OPTIONS.project, undefined, // Because we didn't compile anything. - `${RulesetServiceType.FIREBASE_STORAGE}/bar` + `${RulesetServiceType.FIREBASE_STORAGE}/bar`, ); }); }); diff --git a/src/rulesDeploy.ts b/src/rulesDeploy.ts index 29ab6d9d00f..4d8a0efc597 100644 --- a/src/rulesDeploy.ts +++ b/src/rulesDeploy.ts @@ -1,14 +1,16 @@ -import _ = require("lodash"); -import clc = require("cli-color"); -import fs = require("fs"); +import * as _ from "lodash"; +import { bold } from "colorette"; +import * as fs from "fs-extra"; -import gcp = require("./gcp"); +import * as gcp from "./gcp"; import { logger } from "./logger"; import { FirebaseError } from "./error"; -import utils = require("./utils"); +import * as utils from "./utils"; import { promptOnce } from "./prompt"; import { ListRulesetsEntry, Release, RulesetFile } from "./gcp/rules"; +import { getProjectNumber } from "./getProjectNumber"; +import { addServiceAccountToRoles, serviceAccountHasRoles } from "./gcp/resourceManager"; // The status code the Firebase Rules backend sends to indicate too many rulesets. const QUOTA_EXCEEDED_STATUS_CODE = 429; @@ -19,6 +21,12 @@ const RULESET_COUNT_LIMIT = 1000; // how many old rulesets should we delete to free up quota? const RULESETS_TO_GC = 10; +// Cross service function definition regex +const CROSS_SERVICE_FUNCTIONS = /firestore\.(get|exists)/; + +// Cross service rules for Storage role +const CROSS_SERVICE_RULES_ROLE = "roles/firebaserules.firestoreServiceAgent"; + /** * Services that have rulesets. */ @@ -48,7 +56,10 @@ export class RulesDeploy { * @param options The CLI options object. * @param type The service type for which this ruleset is associated. */ - constructor(public options: any, private type: RulesetServiceType) { + constructor( + public options: any, + private type: RulesetServiceType, + ) { this.project = options.project; this.rulesFiles = {}; this.rulesetNames = {}; @@ -64,9 +75,9 @@ export class RulesDeploy { let src; try { src = fs.readFileSync(fullPath, "utf8"); - } catch (e) { + } catch (e: any) { logger.debug("[rules read error]", e.stack); - throw new FirebaseError("Error reading rules file " + clc.bold(path)); + throw new FirebaseError(`Error reading rules file ${bold(path)}`); } this.rulesFiles[path] = [{ name: path, content: src }]; @@ -80,7 +91,7 @@ export class RulesDeploy { await Promise.all( Object.keys(this.rulesFiles).map((filename) => { return this.compileRuleset(filename, this.rulesFiles[filename]); - }) + }), ); } @@ -90,7 +101,7 @@ export class RulesDeploy { * @return An object containing the latest name and content of the current rules. */ private async getCurrentRules( - service: RulesetServiceType + service: RulesetServiceType, ): Promise<{ latestName: string | null; latestContent: RulesetFile[] | null }> { const latestName = await gcp.rules.getLatestRulesetName(this.options.project, service); let latestContent: RulesetFile[] | null = null; @@ -100,6 +111,52 @@ export class RulesDeploy { return { latestName, latestContent }; } + async checkStorageRulesIamPermissions(rulesContent?: string): Promise { + // Skip if no cross-service rules + if (rulesContent?.match(CROSS_SERVICE_FUNCTIONS) === null) { + return; + } + + // Skip if non-interactive + if (this.options.nonInteractive) { + return; + } + + // We have cross-service rules. Now check the P4SA permission + const projectNumber = await getProjectNumber(this.options); + const saEmail = `service-${projectNumber}@gcp-sa-firebasestorage.iam.gserviceaccount.com`; + try { + if (await serviceAccountHasRoles(projectNumber, saEmail, [CROSS_SERVICE_RULES_ROLE], true)) { + return; + } + + // Prompt user to ask if they want to add the service account + const addRole = await promptOnce( + { + type: "confirm", + name: "rulesRole", + message: `Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?`, + default: true, + }, + this.options, + ); + + // Try to add the role to the service account + if (addRole) { + await addServiceAccountToRoles(projectNumber, saEmail, [CROSS_SERVICE_RULES_ROLE], true); + utils.logLabeledBullet( + RulesetType[this.type], + "updated service account for cross-service rules...", + ); + } + } catch (e: any) { + logger.warn( + "[rules] Error checking or updating Cloud Storage for Firebase service account permissions.", + ); + logger.warn("[rules] Cross-service Storage rules may not function properly", e.message); + } + } + /** * Create rulesets for each file added to this deploy, and record * the name for use in the release process later. @@ -113,28 +170,26 @@ export class RulesDeploy { async createRulesets(service: RulesetServiceType): Promise { const createdRulesetNames: string[] = []; - const { - latestName: latestRulesetName, - latestContent: latestRulesetContent, - } = await this.getCurrentRules(service); + const { latestName: latestRulesetName, latestContent: latestRulesetContent } = + await this.getCurrentRules(service); // TODO: Make this into a more useful helper method. // Gather the files to be uploaded. const newRulesetsByFilename = new Map>(); - for (const filename of Object.keys(this.rulesFiles)) { - const files = this.rulesFiles[filename]; + for (const [filename, files] of Object.entries(this.rulesFiles)) { if (latestRulesetName && _.isEqual(files, latestRulesetContent)) { - utils.logBullet( - `${clc.bold.cyan(RulesetType[this.type] + ":")} latest version of ${clc.bold( - filename - )} already up to date, skipping upload...` + utils.logLabeledBullet( + RulesetType[this.type], + `latest version of ${bold(filename)} already up to date, skipping upload...`, ); this.rulesetNames[filename] = latestRulesetName; continue; } - utils.logBullet( - `${clc.bold.cyan(RulesetType[this.type] + ":")} uploading rules ${clc.bold(filename)}...` - ); + if (service === RulesetServiceType.FIREBASE_STORAGE) { + await this.checkStorageRulesIamPermissions(files[0]?.content); + } + + utils.logLabeledBullet(RulesetType[this.type], `uploading rules ${bold(filename)}...`); newRulesetsByFilename.set(filename, gcp.rules.createRuleset(this.options.project, files)); } @@ -145,14 +200,11 @@ export class RulesDeploy { this.rulesetNames[filename] = await rulesetName; createdRulesetNames.push(await rulesetName); } - } catch (err) { + } catch (err: any) { if (err.status !== QUOTA_EXCEEDED_STATUS_CODE) { throw err; } - utils.logBullet( - clc.bold.yellow(RulesetType[this.type] + ":") + - " quota exceeded error while uploading rules" - ); + utils.logLabeledBullet(RulesetType[this.type], "quota exceeded error while uploading rules"); const history: ListRulesetsEntry[] = await gcp.rules.listAllRulesets(this.options.project); @@ -164,24 +216,21 @@ export class RulesDeploy { message: `You have ${history.length} rules, do you want to delete the oldest ${RULESETS_TO_GC} to free up space?`, default: false, }, - this.options + this.options, ); if (confirm) { // Find the oldest unreleased rulesets. The rulesets are sorted reverse-chronlogically. const releases: Release[] = await gcp.rules.listAllReleases(this.options.project); - const unreleased: ListRulesetsEntry[] = _.reject( - history, - (ruleset: ListRulesetsEntry): boolean => { - return !!releases.find((release) => release.rulesetName === ruleset.name); - } - ); + const unreleased: ListRulesetsEntry[] = history.filter((ruleset) => { + return !releases.find((release) => release.rulesetName === ruleset.name); + }); const entriesToDelete = unreleased.reverse().slice(0, RULESETS_TO_GC); // To avoid running into quota issues, delete entries in _serial_ rather than parallel. for (const entry of entriesToDelete) { await gcp.rules.deleteRuleset(this.options.project, gcp.rules.getRulesetId(entry)); logger.debug(`[rules] Deleted ${entry.name}`); } - utils.logBullet(clc.bold.yellow(RulesetType[this.type] + ":") + " retrying rules upload"); + utils.logLabeledWarning(RulesetType[this.type], "retrying rules upload"); return this.createRulesets(service); } } @@ -194,12 +243,12 @@ export class RulesDeploy { * @param filename The filename to release. * @param resourceName The release name to release these as. * @param subResourceName An optional sub-resource name to append to the - * release name. This is required if resourceName == FIREBASE_STORAGE. + * release name. This is required if resourceName === FIREBASE_STORAGE. */ async release( filename: string, resourceName: RulesetServiceType, - subResourceName?: string + subResourceName?: string, ): Promise { // Cast as a RulesetServiceType to test the value against known types. if (resourceName === RulesetServiceType.FIREBASE_STORAGE && !subResourceName) { @@ -208,14 +257,11 @@ export class RulesDeploy { await gcp.rules.updateOrCreateRelease( this.options.project, this.rulesetNames[filename], - resourceName === RulesetServiceType.FIREBASE_STORAGE - ? `${resourceName}/${subResourceName}` - : resourceName + subResourceName ? `${resourceName}/${subResourceName}` : resourceName, ); - utils.logSuccess( - `${clc.bold.green(RulesetType[this.type] + ":")} released rules ${clc.bold( - filename - )} to ${clc.bold(resourceName)}` + utils.logLabeledSuccess( + RulesetType[this.type], + `released rules ${bold(filename)} to ${bold(resourceName)}`, ); } @@ -225,9 +271,7 @@ export class RulesDeploy { * @param files The files to compile. */ private async compileRuleset(filename: string, files: RulesetFile[]): Promise { - utils.logBullet( - `${clc.bold.cyan(this.type + ":")} checking ${clc.bold(filename)} for compilation errors...` - ); + utils.logLabeledBullet(this.type, `checking ${bold(filename)} for compilation errors...`); const response = await gcp.rules.testRuleset(this.options.project, files); if (_.get(response, "body.issues", []).length) { const warnings: string[] = []; @@ -252,13 +296,11 @@ export class RulesDeploy { if (errors.length > 0) { const add = errors.length === 1 ? "" : "s"; - const message = `Compilation error${add} in ${clc.bold(filename)}:\n${errors.join("\n")}`; + const message = `Compilation error${add} in ${bold(filename)}:\n${errors.join("\n")}`; throw new FirebaseError(message, { exit: 1 }); } } - utils.logSuccess( - `${clc.bold.green(this.type + ":")} rules file ${clc.bold(filename)} compiled successfully` - ); + utils.logLabeledSuccess(this.type, `rules file ${bold(filename)} compiled successfully`); } } diff --git a/src/scopes.js b/src/scopes.js deleted file mode 100644 index 2877c44516e..00000000000 --- a/src/scopes.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -module.exports = { - // default scopes - OPENID: "openid", - EMAIL: "email", - CLOUD_PROJECTS_READONLY: "https://www.googleapis.com/auth/cloudplatformprojects.readonly", - FIREBASE_PLATFORM: "https://www.googleapis.com/auth/firebase", - - // incremental scopes - CLOUD_PLATFORM: "https://www.googleapis.com/auth/cloud-platform", - CLOUD_STORAGE: "https://www.googleapis.com/auth/devstorage.read_write", - CLOUD_PUBSUB: "https://www.googleapis.com/auth/pubsub", -}; diff --git a/src/scopes.ts b/src/scopes.ts new file mode 100644 index 00000000000..9bdef330e61 --- /dev/null +++ b/src/scopes.ts @@ -0,0 +1,11 @@ +// default scopes +export const OPENID = "openid"; +export const EMAIL = "email"; +export const CLOUD_PROJECTS_READONLY = + "https://www.googleapis.com/auth/cloudplatformprojects.readonly"; +export const FIREBASE_PLATFORM = "https://www.googleapis.com/auth/firebase"; + +// incremental scopes +export const CLOUD_PLATFORM = "https://www.googleapis.com/auth/cloud-platform"; +export const CLOUD_STORAGE = "https://www.googleapis.com/auth/devstorage.read_write"; +export const CLOUD_PUBSUB = "https://www.googleapis.com/auth/pubsub"; diff --git a/src/serve/functions.ts b/src/serve/functions.ts index f8c2d77792c..4cefea276bf 100644 --- a/src/serve/functions.ts +++ b/src/serve/functions.ts @@ -1,47 +1,60 @@ import * as path from "path"; -import { FunctionsEmulator, FunctionsEmulatorArgs } from "../emulator/functionsEmulator"; -import { EmulatorServer } from "../emulator/emulatorServer"; -import { parseRuntimeVersion } from "../emulator/functionsEmulatorUtils"; +import { + EmulatableBackend, + FunctionsEmulator, + FunctionsEmulatorArgs, +} from "../emulator/functionsEmulator"; import { needProjectId } from "../projectUtils"; import { getProjectDefaultAccount } from "../auth"; import { Options } from "../options"; -import { Config } from "../config"; +import * as projectConfig from "../functions/projectConfig"; import * as utils from "../utils"; +import { EmulatorRegistry } from "../emulator/registry"; +import { parseInspectionPort } from "../emulator/commandUtils"; -// TODO(samstern): It would be better to convert this to an EmulatorServer -// but we don't have the "options" object until start() is called. export class FunctionsServer { - emulatorServer: EmulatorServer | undefined = undefined; + emulator?: FunctionsEmulator; + backends?: EmulatableBackend[]; - private assertServer() { - if (!this.emulatorServer) { + private assertServer(): FunctionsEmulator { + if (!this.emulator || !this.backends) { throw new Error("Must call start() before calling any other operation!"); } + return this.emulator; } async start(options: Options, partialArgs: Partial): Promise { const projectId = needProjectId(options); - utils.assertDefined(options.config.src.functions); - utils.assertDefined( - options.config.src.functions.source, - "Error: 'functions.source' is not defined" - ); + const config = projectConfig.normalizeAndValidate(options.config.src.functions); - const functionsDir = path.join(options.config.projectDir, options.config.src.functions.source); - const account = getProjectDefaultAccount(options.config.projectDir); - const nodeMajorVersion = parseRuntimeVersion(options.config.get("functions.runtime")); + const backends: EmulatableBackend[] = []; + for (const cfg of config) { + const functionsDir = path.join(options.config.projectDir, cfg.source); + backends.push({ + functionsDir, + codebase: cfg.codebase, + runtime: cfg.runtime, + env: {}, + secretEnv: [], + }); + } + this.backends = backends; - // Normally, these two fields are included in args (and typed as such). - // However, some poorly-typed tests may not have them and we need to provide - // default values for those tests to work properly. + const account = getProjectDefaultAccount(options.config.projectDir); const args: FunctionsEmulatorArgs = { projectId, - functionsDir, + projectDir: options.config.projectDir, + emulatableBackends: this.backends, + projectAlias: options.projectAlias, account, - nodeMajorVersion, ...partialArgs, + // Non-optional; parseInspectionPort will set to false if missing. + debugPort: parseInspectionPort(options), }; + // Normally, these two fields are included in args (and typed as such). + // However, some poorly-typed tests may not have them and we need to provide + // default values for those tests to work properly. if (options.host) { utils.assertIsStringOrUndefined(options.host); args.host = options.host; @@ -54,7 +67,7 @@ export class FunctionsServer { utils.assertIsNumber(options.port); const targets = options.targets as string[] | undefined; const port = options.port; - const hostingRunning = targets && targets.indexOf("hosting") >= 0; + const hostingRunning = targets && targets.includes("hosting"); if (hostingRunning) { args.port = port + 1; } else { @@ -62,22 +75,19 @@ export class FunctionsServer { } } - this.emulatorServer = new EmulatorServer(new FunctionsEmulator(args)); - await this.emulatorServer.start(); + this.emulator = new FunctionsEmulator(args); + return EmulatorRegistry.start(this.emulator); } async connect(): Promise { - this.assertServer(); - await this.emulatorServer!.connect(); + await this.assertServer().connect(); } async stop(): Promise { - this.assertServer(); - await this.emulatorServer!.stop(); + await this.assertServer().stop(); } get(): FunctionsEmulator { - this.assertServer(); - return this.emulatorServer!.get() as FunctionsEmulator; + return this.assertServer(); } } diff --git a/src/serve/hosting.ts b/src/serve/hosting.ts index 0920fe5ec1a..57a06e5fe52 100644 --- a/src/serve/hosting.ts +++ b/src/serve/hosting.ts @@ -1,23 +1,24 @@ -import clc = require("cli-color"); - -const superstatic = require("superstatic").server; // Superstatic has no types, requires odd importing. const morgan = require("morgan"); +import { isIPv4 } from "net"; +import { server as superstatic } from "superstatic"; +import * as clc from "colorette"; import { detectProjectRoot } from "../detectProjectRoot"; import { FirebaseError } from "../error"; import { implicitInit, TemplateServerResponse } from "../hosting/implicitInit"; import { initMiddleware } from "../hosting/initMiddleware"; -import { normalizedHostingConfigs } from "../hosting/normalizedHostingConfigs"; +import * as config from "../hosting/config"; import cloudRunProxy from "../hosting/cloudRunProxy"; -import functionsProxy from "../hosting/functionsProxy"; -import { NextFunction, Request, Response } from "express"; +import { functionsProxy } from "../hosting/functionsProxy"; import { Writable } from "stream"; import { EmulatorLogger } from "../emulator/emulatorLogger"; import { Emulators } from "../emulator/types"; import { createDestroyer } from "../utils"; +import { requireHostingSite } from "../requireHostingSite"; +import { getProjectId } from "../projectUtils"; +import { checkListenable } from "../emulator/portUtils"; +import { IncomingMessage, ServerResponse } from "http"; -const MAX_PORT_ATTEMPTS = 10; -let attempts = 0; let destroyServer: undefined | (() => Promise) = undefined; const logger = EmulatorLogger.forEmulator(Emulators.HOSTING); @@ -31,7 +32,7 @@ function startServer(options: any, config: any, port: number, init: TemplateServ morganStream._write = ( chunk: any, encoding: string, - callback: (error?: Error | null) => void + callback: (error?: Error | null) => void, ) => { if (chunk instanceof Buffer) { logger.logLabeled("BULLET", "hosting", chunk.toString().trim()); @@ -44,24 +45,26 @@ function startServer(options: any, config: any, port: number, init: TemplateServ stream: morganStream, }); + const after = options.frameworksDevModeHandle && { + files: options.frameworksDevModeHandle, + }; + const server = superstatic({ debug: false, port: port, - host: options.host, + hostname: options.host, config: config, - cwd: detectProjectRoot(options), + compression: true, + cwd: detectProjectRoot(options) || undefined, stack: "strict", before: { - files: (req: Request, res: Response, next: NextFunction) => { + files: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { // We do these in a single method to ensure order of operations - morganMiddleware(req, res, () => { - /* - NoOp next function - */ - }); + morganMiddleware(req, res, () => null); firebaseMiddleware(req, res, next); }, }, + after, rewriters: { function: functionsProxy(options), run: cloudRunProxy(options), @@ -76,33 +79,17 @@ function startServer(options: any, config: any, port: number, init: TemplateServ logger.logLabeled( "SUCCESS", label, - "Local server: " + clc.underline(clc.bold("http://" + options.host + ":" + port)) + "Local server: " + clc.underline(clc.bold("http://" + options.host + ":" + port)), ); }); destroyServer = createDestroyer(server); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server.on("error", (err: any) => { - if (err.code === "EADDRINUSE") { - const message = "Port " + options.port + " is not available."; - logger.log("WARN", clc.yellow("hosting: ") + message + " Trying another port..."); - if (attempts < MAX_PORT_ATTEMPTS) { - // Another project that's running takes up to 4 ports: 1 hosting port and 3 functions ports - attempts++; - startServer(options, config, port + 5, init); - } else { - logger.log("WARN", message); - throw new FirebaseError("Could not find an open port for hosting development server.", { - exit: 1, - }); - } - } else { - throw new FirebaseError( - "An error occurred while starting the hosting development server:\n\n" + err.toString(), - { exit: 1 } - ); - } + server.on("error", (err: Error) => { + logger.log("DEBUG", `Error from superstatic server: ${err.stack || ""}`); + throw new FirebaseError( + `An error occurred while starting the hosting development server:\n\n${err.message}`, + ); }); } @@ -118,15 +105,45 @@ export function stop(): Promise { * @param options the Firebase CLI options. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function start(options: any): Promise { +export async function start(options: any): Promise<{ ports: number[] }> { const init = await implicitInit(options); - const configs = normalizedHostingConfigs(options); + // N.B. Originally we didn't call this method because it could try to resolve + // targets and cause us to fail. But we might be calling prepareFrameworks, + // which modifies the cached result of config.hostingConfig. So if we don't + // call this, we won't get web frameworks. But we might need to change this + // as well to avoid validation errors. + // But hostingConfig tries to resolve targets and a customer might not have + // site/targets defined + if (!options.site) { + try { + await requireHostingSite(options); + } catch { + if (init.json) { + options.site = JSON.parse(init.json).projectId; + } else { + options.site = getProjectId(options) || "site"; + } + } + } + const configs = config.hostingConfig(options); + // We never want to try and take port 5001 because Functions likes that port + // quite a bit, and we don't want to make Functions mad. + const assignedPorts = new Set([5001]); for (let i = 0; i < configs.length; i++) { // skip over the functions emulator ports to avoid breaking changes - const port = i === 0 ? options.port : options.port + 4 + i; + let port = i === 0 ? options.port : options.port + 4 + i; + while (assignedPorts.has(port) || !(await availablePort(options.host, port))) { + port += 1; + } + assignedPorts.add(port); startServer(options, configs[i], port, init); } + + // We are not actually reserving 5001, so remove it from our set before + // returning. + assignedPorts.delete(5001); + return { ports: Array.from(assignedPorts) }; } /** @@ -135,3 +152,11 @@ export async function start(options: any): Promise { export async function connect(): Promise { await Promise.resolve(); } + +function availablePort(host: string, port: number): Promise { + return checkListenable({ + address: host, + port, + family: isIPv4(host) ? "IPv4" : "IPv6", + }); +} diff --git a/src/serve/index.ts b/src/serve/index.ts index 79699a80bb9..20583883a83 100644 --- a/src/serve/index.ts +++ b/src/serve/index.ts @@ -1,13 +1,15 @@ -import { EmulatorServer } from "../emulator/emulatorServer"; -import * as _ from "lodash"; import { logger } from "../logger"; +import { prepareFrameworks } from "../frameworks"; +import * as experiments from "../experiments"; +import { trackEmulator } from "../track"; +import { getProjectId } from "../projectUtils"; +import { Constants } from "../emulator/constants"; +import * as config from "../hosting/config"; const { FunctionsServer } = require("./functions"); const TARGETS: { - [key: string]: - | EmulatorServer - | { start: (o: any) => void; stop: (o: any) => void; connect: () => void }; + [key: string]: { start: (o: any) => void; stop: (o: any) => void; connect: () => void }; } = { hosting: require("./hosting"), functions: new FunctionsServer(), @@ -18,25 +20,42 @@ const TARGETS: { * @param options Firebase CLI options. */ export async function serve(options: any): Promise { - const targetNames = options.targets; + options.targets ||= []; + const targetNames: string[] = options.targets; options.port = parseInt(options.port, 10); + if (targetNames.includes("hosting") && config.extract(options).some((it: any) => it.source)) { + experiments.assertEnabled("webframeworks", "emulate a web framework"); + await prepareFrameworks("emulate", targetNames, undefined, options); + } + const isDemoProject = Constants.isDemoProject(getProjectId(options) || ""); + targetNames.forEach((targetName) => { + void trackEmulator("emulator_run", { + emulator_name: targetName, + is_demo_project: String(isDemoProject), + }); + }); await Promise.all( - _.map(targetNames, (targetName: string) => { + targetNames.map((targetName: string) => { return TARGETS[targetName].start(options); - }) + }), ); await Promise.all( - _.map(targetNames, (targetName: string) => { + targetNames.map((targetName: string) => { return TARGETS[targetName].connect(); - }) + }), ); + void trackEmulator("emulators_started", { + count: targetNames.length, + count_all: targetNames.length, + is_demo_project: String(isDemoProject), + }); await new Promise((resolve) => { process.on("SIGINT", () => { logger.info("Shutting down..."); - return Promise.all( - _.map(targetNames, (targetName: string) => { + Promise.all( + targetNames.map((targetName: string) => { return TARGETS[targetName].stop(options); - }) + }), ) .then(resolve) .catch(resolve); diff --git a/src/test/shortenUrl.spec.ts b/src/shortenUrl.spec.ts similarity index 85% rename from src/test/shortenUrl.spec.ts rename to src/shortenUrl.spec.ts index a5c5012ee70..66cf230903e 100644 --- a/src/test/shortenUrl.spec.ts +++ b/src/shortenUrl.spec.ts @@ -1,20 +1,20 @@ import { expect } from "chai"; import * as nock from "nock"; -import { dynamicLinksKey, dynamicLinksOrigin } from "../api"; -import { shortenUrl } from "../shortenUrl"; +import { dynamicLinksKey, dynamicLinksOrigin } from "./api"; +import { shortenUrl } from "./shortenUrl"; describe("shortenUrl", () => { const TEST_LINK = "https://abc.def/"; const MOCKED_LINK = "https://firebase.tools/l/TEST"; function mockDynamicLinks(url: string, suffix = "UNGUESSABLE", code = 200): void { - nock(dynamicLinksOrigin) + nock(dynamicLinksOrigin()) .post( `/v1/shortLinks`, (body: { dynamicLinkInfo?: { link: string }; suffix?: { option: string } }) => - body.dynamicLinkInfo?.link === url && body.suffix?.option === suffix + body.dynamicLinkInfo?.link === url && body.suffix?.option === suffix, ) - .query({ key: dynamicLinksKey }) + .query({ key: dynamicLinksKey() }) .reply(code, { shortLink: MOCKED_LINK, previewLink: `${MOCKED_LINK}?d=1`, diff --git a/src/shortenUrl.ts b/src/shortenUrl.ts index e69bad7f4d7..336e849ef29 100644 --- a/src/shortenUrl.ts +++ b/src/shortenUrl.ts @@ -5,7 +5,7 @@ import { dynamicLinksKey, dynamicLinksOrigin } from "./api"; const DYNAMIC_LINKS_PREFIX = "https://firebase.tools/l"; const apiClient = new Client({ - urlPrefix: dynamicLinksOrigin, + urlPrefix: dynamicLinksOrigin(), auth: false, apiVersion: "v1", }); @@ -33,18 +33,18 @@ interface DynamicLinksResponse { export async function shortenUrl(url: string, guessable = false): Promise { try { const response = await apiClient.post( - `shortLinks?key=${dynamicLinksKey}`, + `shortLinks?key=${dynamicLinksKey()}`, { dynamicLinkInfo: { link: url, domainUriPrefix: DYNAMIC_LINKS_PREFIX, }, suffix: { option: guessable ? "SHORT" : "UNGUESSABLE" }, - } + }, ); return response.body.shortLink; - } catch (e) { + } catch (e: any) { logger.debug("URL shortening failed, falling back to full URL. Error:", e.original || e); return url; } diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 00000000000..151bf73752f --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,37 @@ +import { readFileSync } from "fs"; +import { readFile } from "fs/promises"; +import { resolve } from "path"; +import { isVSCodeExtension } from "./utils"; + +const TEMPLATE_ENCODING = "utf8"; + +/** + * Get an absolute template file path. (Prefer readTemplateSync instead.) + * @param relPath file path relative to the /templates directory under root. + */ +export function absoluteTemplateFilePath(relPath: string): string { + if (isVSCodeExtension()) { + // In the VSCE, the /templates directory is copied into dist, which makes it + // right next to the compiled files (from various sources including this + // TS file). See CopyPlugin in `../firebase-vscode/webpack.common.js`. + return resolve(__dirname, "templates", relPath); + } + // Otherwise, the /templates directory is one level above /src or /lib. + return resolve(__dirname, "../templates", relPath); +} + +/** + * Read a template file synchronously. + * @param relPath file path relative to the /templates directory under root. + */ +export function readTemplateSync(relPath: string): string { + return readFileSync(absoluteTemplateFilePath(relPath), TEMPLATE_ENCODING); +} + +/** + * Read a template file asynchronously. + * @param relPath file path relative to the /templates directory under root. + */ +export function readTemplate(relPath: string): Promise { + return readFile(absoluteTemplateFilePath(relPath), TEMPLATE_ENCODING); +} diff --git a/src/test/accountExporter.spec.js b/src/test/accountExporter.spec.js deleted file mode 100644 index ea18ee7edc7..00000000000 --- a/src/test/accountExporter.spec.js +++ /dev/null @@ -1,287 +0,0 @@ -"use strict"; - -var chai = require("chai"); -var nock = require("nock"); -var os = require("os"); -var sinon = require("sinon"); - -var accountExporter = require("../accountExporter"); - -var expect = chai.expect; -describe("accountExporter", function () { - var validateOptions = accountExporter.validateOptions; - var serialExportUsers = accountExporter.serialExportUsers; - - describe("validateOptions", function () { - it("should reject when no format provided", function () { - return expect(() => validateOptions({}, "output_file")).to.throw; - }); - - it("should reject when format is not csv or json", function () { - return expect(() => validateOptions({ format: "txt" }, "output_file")).to.throw; - }); - - it("should ignore format param when implicitly specified in file name", function () { - var ret = validateOptions({ format: "JSON" }, "output_file.csv"); - expect(ret.format).to.eq("csv"); - }); - - it("should use format param when not implicitly specified in file name", function () { - var ret = validateOptions({ format: "JSON" }, "output_file"); - expect(ret.format).to.eq("json"); - }); - }); - - describe("serialExportUsers", function () { - var sandbox; - var userList = []; - var writeStream = { - write: function () {}, - end: function () {}, - }; - var spyWrite; - - beforeEach(function () { - sandbox = sinon.createSandbox(); - spyWrite = sandbox.spy(writeStream, "write"); - for (var i = 0; i < 7; i++) { - userList.push({ - localId: i.toString(), - email: "test" + i + "@test.org", - displayName: "John Tester" + i, - disabled: i % 2 === 0, - }); - } - }); - - afterEach(function () { - sandbox.restore(); - nock.cleanAll(); - userList = []; - }); - - it("should call api.request multiple times for JSON export", function () { - nock("https://www.googleapis.com") - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - targetProjectId: "test-project-id", - }) - .reply(200, { - users: userList.slice(0, 3), - nextPageToken: "3", - }) - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - nextPageToken: "3", - targetProjectId: "test-project-id", - }) - .reply(200, { - users: userList.slice(3, 6), - nextPageToken: "6", - }) - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - nextPageToken: "6", - targetProjectId: "test-project-id", - }) - .reply(200, { - users: userList.slice(6, 7), - nextPageToken: "7", - }) - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - nextPageToken: "7", - targetProjectId: "test-project-id", - }) - .reply(200, { - users: [], - nextPageToken: "7", - }); - - return serialExportUsers("test-project-id", { - format: "JSON", - batchSize: 3, - writeStream: writeStream, - }).then(function () { - expect(spyWrite.callCount).to.eq(7); - expect(spyWrite.getCall(0).args[0]).to.eq(JSON.stringify(userList[0], null, 2)); - for (var j = 1; j < 7; j++) { - expect(spyWrite.getCall(j).args[0]).to.eq( - "," + os.EOL + JSON.stringify(userList[j], null, 2) - ); - } - }); - }); - - it("should call api.request multiple times for CSV export", function () { - mockAllUsersRequests(); - - return serialExportUsers("test-project-id", { - format: "csv", - batchSize: 3, - writeStream: writeStream, - }).then(function () { - expect(spyWrite.callCount).to.eq(userList.length); - for (var j = 0; j < userList.length; j++) { - var expectedEntry = - userList[j].localId + - "," + - userList[j].email + - ",false,,," + - userList[j].displayName + - Array(22).join(",") + // A lot of empty fields... - userList[j].disabled; - expect(spyWrite.getCall(j).args[0]).to.eq(expectedEntry + ",," + os.EOL); - } - }); - }); - - it("should encapsulate displayNames with commas for csv formats", function () { - // Initialize user with comma in display name. - var singleUser = { - localId: "1", - email: "test1@test.org", - displayName: "John Tester1, CFA", - disabled: false, - }; - nock("https://www.googleapis.com") - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 1, - targetProjectId: "test-project-id", - }) - .reply(200, { - users: [singleUser], - nextPageToken: "1", - }) - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 1, - nextPageToken: "1", - targetProjectId: "test-project-id", - }) - .reply(200, { - users: [], - nextPageToken: "1", - }); - - return serialExportUsers("test-project-id", { - format: "csv", - batchSize: 1, - writeStream: writeStream, - }).then(function () { - expect(spyWrite.callCount).to.eq(1); - var expectedEntry = - singleUser.localId + - "," + - singleUser.email + - ",false,,," + - '"' + - singleUser.displayName + - '"' + - Array(22).join(",") + // A lot of empty fields. - singleUser.disabled; - expect(spyWrite.getCall(0).args[0]).to.eq(expectedEntry + ",," + os.EOL); - }); - }); - - it("should not emit redundant comma in JSON on consecutive calls", function () { - mockAllUsersRequests(); - - const correctString = - '{\n "localId": "0",\n "email": "test0@test.org",\n "displayName": "John Tester0",\n "disabled": true\n}'; - - const firstWriteSpy = sinon.spy(); - return serialExportUsers("test-project-id", { - format: "JSON", - batchSize: 3, - writeStream: { write: firstWriteSpy, end: function () {} }, - }).then(function () { - expect(firstWriteSpy.args[0][0]).to.be.eq( - correctString, - "The first call did not emit the correct string" - ); - - mockAllUsersRequests(); - - const secondWriteSpy = sinon.spy(); - return serialExportUsers("test-project-id", { - format: "JSON", - batchSize: 3, - writeStream: { write: secondWriteSpy, end: function () {} }, - }).then(() => { - expect(secondWriteSpy.args[0][0]).to.be.eq( - correctString, - "The second call did not emit the correct string" - ); - }); - }); - }); - - it("should export a user's custom attributes", function () { - userList[0].customAttributes = - '{ "customBoolean": true, "customString": "test", "customInt": 99 }'; - userList[1].customAttributes = - '{ "customBoolean": true, "customString2": "test2", "customInt": 99 }'; - nock("https://www.googleapis.com") - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - targetProjectId: "test-project-id", - }) - .reply(200, { - users: userList.slice(0, 3), - nextPageToken: "3", - }); - return serialExportUsers("test-project-id", { - format: "JSON", - batchSize: 3, - writeStream: writeStream, - }).then(function () { - expect(spyWrite.getCall(0).args[0]).to.eq(JSON.stringify(userList[0], null, 2)); - expect(spyWrite.getCall(1).args[0]).to.eq( - "," + os.EOL + JSON.stringify(userList[1], null, 2) - ); - expect(spyWrite.getCall(2).args[0]).to.eq( - "," + os.EOL + JSON.stringify(userList[2], null, 2) - ); - }); - }); - - function mockAllUsersRequests() { - nock("https://www.googleapis.com") - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - targetProjectId: "test-project-id", - }) - .reply(200, { - users: userList.slice(0, 3), - nextPageToken: "3", - }) - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - nextPageToken: "3", - targetProjectId: "test-project-id", - }) - .reply(200, { - users: userList.slice(3, 6), - nextPageToken: "6", - }) - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - nextPageToken: "6", - targetProjectId: "test-project-id", - }) - .reply(200, { - users: userList.slice(6, 7), - nextPageToken: "7", - }) - .post("/identitytoolkit/v3/relyingparty/downloadAccount", { - maxResults: 3, - nextPageToken: "7", - targetProjectId: "test-project-id", - }) - .reply(200, { - users: [], - nextPageToken: "7", - }); - } - }); -}); diff --git a/src/test/accountImporter.spec.js b/src/test/accountImporter.spec.js deleted file mode 100644 index 4ce0e7fca17..00000000000 --- a/src/test/accountImporter.spec.js +++ /dev/null @@ -1,193 +0,0 @@ -"use strict"; - -var chai = require("chai"); -var sinon = require("sinon"); -var api = require("../api"); -var accountImporter = require("../accountImporter"); - -var expect = chai.expect; -describe("accountImporter", function () { - var transArrayToUser = accountImporter.transArrayToUser; - var validateOptions = accountImporter.validateOptions; - var validateUserJson = accountImporter.validateUserJson; - var serialImportUsers = accountImporter.serialImportUsers; - - describe("transArrayToUser", function () { - it("should reject when passwordHash is invalid base64", function () { - return expect(transArrayToUser(["123", undefined, undefined, "false"])).to.have.property( - "error" - ); - }); - - it("should not reject when passwordHash is valid base64", function () { - return expect( - transArrayToUser(["123", undefined, undefined, "Jlf7onfLbzqPNFP/1pqhx6fQF/w="]) - ).to.not.have.property("error"); - }); - }); - - describe("validateOptions", function () { - it("should reject when unsupported hash algorithm provided", function () { - return expect(() => validateOptions({ hashAlgo: "MD2" })).to.throw; - }); - - it("should reject when missing parameters", function () { - return expect(() => validateOptions({ hashAlgo: "HMAC_SHA1" })).to.throw; - }); - }); - - describe("validateUserJson", function () { - it("should reject when unknown fields in user json", function () { - return expect( - validateUserJson({ - uid: "123", - email: "test@test.org", - }) - ).to.have.property("error"); - }); - - it("should reject when unknown fields in providerUserInfo of user json", function () { - return expect( - validateUserJson({ - localId: "123", - email: "test@test.org", - providerUserInfo: [ - { - providerId: "google.com", - googleId: "abc", - email: "test@test.org", - }, - ], - }) - ).to.have.property("error"); - }); - - it("should reject when unknown providerUserInfo of user json", function () { - return expect( - validateUserJson({ - localId: "123", - email: "test@test.org", - providerUserInfo: [ - { - providerId: "otheridp.com", - rawId: "abc", - email: "test@test.org", - }, - ], - }) - ).to.have.property("error"); - }); - - it("should reject when passwordHash is invalid base64", function () { - return expect( - validateUserJson({ - localId: "123", - passwordHash: "false", - }) - ).to.have.property("error"); - }); - - it("should not reject when passwordHash is valid base64", function () { - return expect( - validateUserJson({ - localId: "123", - passwordHash: "Jlf7onfLbzqPNFP/1pqhx6fQF/w=", - }) - ).to.not.have.property("error"); - }); - }); - - describe("serialImportUsers", function () { - var sandbox; - var mockApi; - var batches = []; - var hashOptions = { - hashAlgo: "HMAC_SHA1", - hashKey: "a2V5MTIz", - }; - var expectedResponse = []; - - beforeEach(function () { - sandbox = sinon.createSandbox(); - mockApi = sandbox.mock(api); - for (var i = 0; i < 10; i++) { - batches.push([ - { - localId: i.toString(), - email: "test" + i + "@test.org", - }, - ]); - expectedResponse.push({ - status: 200, - response: "", - body: "", - }); - } - }); - - afterEach(function () { - mockApi.verify(); - sandbox.restore(); - batches = []; - expectedResponse = []; - }); - - it("should call api.request multiple times", function (done) { - for (var i = 0; i < batches.length; i++) { - mockApi - .expects("request") - .withArgs("POST", "/identitytoolkit/v3/relyingparty/uploadAccount", { - auth: true, - data: { - hashAlgorithm: "HMAC_SHA1", - signerKey: "a2V5MTIz", - targetProjectId: "test-project-id", - users: [{ email: "test" + i + "@test.org", localId: i.toString() }], - }, - json: true, - origin: "https://www.googleapis.com", - }) - .once() - .resolves(expectedResponse[i]); - } - return expect( - serialImportUsers("test-project-id", hashOptions, batches, 0) - ).to.eventually.notify(done); - }); - - it("should continue when some request's response is 200 but has `error` in response", function (done) { - expectedResponse[5] = { - status: 200, - response: "", - body: { - error: [ - { - index: 0, - message: "some error message", - }, - ], - }, - }; - for (var i = 0; i < batches.length; i++) { - mockApi - .expects("request") - .withArgs("POST", "/identitytoolkit/v3/relyingparty/uploadAccount", { - auth: true, - data: { - hashAlgorithm: "HMAC_SHA1", - signerKey: "a2V5MTIz", - targetProjectId: "test-project-id", - users: [{ email: "test" + i + "@test.org", localId: i.toString() }], - }, - json: true, - origin: "https://www.googleapis.com", - }) - .once() - .resolves(expectedResponse[i]); - } - return expect( - serialImportUsers("test-project-id", hashOptions, batches, 0) - ).to.eventually.notify(done); - }); - }); -}); diff --git a/src/test/api.spec.ts b/src/test/api.spec.ts deleted file mode 100644 index 192b57960e4..00000000000 --- a/src/test/api.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect } from "chai"; - -import * as utils from "../utils"; - -describe("api", () => { - beforeEach(() => { - // The api module resolves env var statically so we need to - // do lazy imports and clear the import each time. - delete require.cache[require.resolve("../api")]; - }); - - afterEach(() => { - delete process.env.FIRESTORE_EMULATOR_HOST; - delete process.env.FIRESTORE_URL; - - // This is dirty, but utils keeps stateful overrides and we need to clear it - utils.envOverrides.length = 0; - }); - - after(() => { - delete require.cache[require.resolve("../api")]; - }); - - it("should override with FIRESTORE_URL", () => { - process.env.FIRESTORE_URL = "http://foobar.com"; - - const api = require("../api"); - expect(api.firestoreOrigin).to.eq("http://foobar.com"); - }); - - it("should prefer FIRESTORE_EMULATOR_HOST to FIRESTORE_URL", () => { - process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080"; - process.env.FIRESTORE_URL = "http://foobar.com"; - - const api = require("../api"); - expect(api.firestoreOriginOrEmulator).to.eq("http://localhost:8080"); - }); -}); diff --git a/src/test/apiv2.spec.ts b/src/test/apiv2.spec.ts deleted file mode 100644 index 3510e056742..00000000000 --- a/src/test/apiv2.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { createServer, Server } from "http"; -import { expect } from "chai"; -import * as nock from "nock"; -import AbortController from "abort-controller"; -import proxySetup = require("proxy"); - -import { Client } from "../apiv2"; -import { FirebaseError } from "../error"; -import { streamToString, stringToStream } from "../utils"; - -describe("apiv2", () => { - beforeEach(() => { - // The api module has package variables that we don't want sticking around. - delete require.cache[require.resolve("../apiv2")]; - - nock.cleanAll(); - }); - - after(() => { - delete require.cache[require.resolve("../apiv2")]; - }); - - describe("request", () => { - it("should throw on a basic 404 GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(404, { message: "not found" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = c.request({ - method: "GET", - path: "/path/to/foo", - }); - await expect(r).to.eventually.be.rejectedWith(FirebaseError, /Not Found/); - expect(nock.isDone()).to.be.true; - }); - - it("should be able to resolve on a 404 GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(404, { message: "not found" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - resolveOnHTTPError: true, - }); - expect(r.status).to.equal(404); - expect(r.body).to.deep.equal({ message: "not found" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - }); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should not allow resolving on http error when streaming", async () => { - const c = new Client({ urlPrefix: "https://example.com" }); - const r = c.request({ - method: "GET", - path: "/path/to/foo", - responseType: "stream", - resolveOnHTTPError: false, - }); - await expect(r).to.eventually.be.rejectedWith(FirebaseError, /streaming.+resolveOnHTTPError/); - }); - - it("should be able to stream a GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(200, "ablobofdata"); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - responseType: "stream", - resolveOnHTTPError: true, - }); - const data = await streamToString(r.body); - expect(data).to.deep.equal("ablobofdata"); - expect(nock.isDone()).to.be.true; - }); - - it("should set a bearer token to 'owner' if making an insecure, local request", async () => { - nock("http://localhost") - .get("/path/to/foo") - .matchHeader("Authorization", "Bearer owner") - .reply(200, { request: "insecure" }); - - const c = new Client({ urlPrefix: "http://localhost" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - }); - expect(r.body).to.deep.equal({ request: "insecure" }); - expect(nock.isDone()).to.be.true; - }); - - it("should error with a FirebaseError if JSON is malformed", async () => { - nock("https://example.com").get("/path/to/foo").reply(200, `{not:"json"}`); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = c.request({ - method: "GET", - path: "/path/to/foo", - }); - await expect(r).to.eventually.be.rejectedWith(FirebaseError, /Unexpected token.+JSON/); - expect(nock.isDone()).to.be.true; - }); - - it("should error with a FirebaseError if an error happens", async () => { - nock("https://example.com").get("/path/to/foo").replyWithError("boom"); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = c.request({ - method: "GET", - path: "/path/to/foo", - }); - await expect(r).to.eventually.be.rejectedWith(FirebaseError, /Failed to make request.+/); - expect(nock.isDone()).to.be.true; - }); - - it("should error with a FirebaseError if an invalid responseType is provided", async () => { - nock("https://example.com").get("/path/to/foo").reply(200, ""); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = c.request({ - method: "GET", - path: "/path/to/foo", - // Don't really do this. This is for testing only. - responseType: "notjson" as "json", - }); - await expect(r).to.eventually.be.rejectedWith( - FirebaseError, - /Unable to interpret response.+/ - ); - expect(nock.isDone()).to.be.true; - }); - - it("should resolve a 400 GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(400, "who dis?"); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - responseType: "stream", - resolveOnHTTPError: true, - }); - expect(r.status).to.equal(400); - expect(await streamToString(r.body)).to.equal("who dis?"); - expect(nock.isDone()).to.be.true; - }); - - it("should resolve a 404 GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(404, "not here"); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - responseType: "stream", - resolveOnHTTPError: true, - }); - expect(r.status).to.equal(404); - expect(await streamToString(r.body)).to.equal("not here"); - expect(nock.isDone()).to.be.true; - }); - - it("should be able to resolve a stream on a 404 GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(404, "does not exist"); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - responseType: "stream", - resolveOnHTTPError: true, - }); - const data = await streamToString(r.body); - expect(data).to.deep.equal("does not exist"); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic GET request if path didn't include a leading slash", async () => { - nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "path/to/foo", - }); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic GET request if urlPrefix did have a trailing slash", async () => { - nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com/" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - }); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic GET request with an api version", async () => { - nock("https://example.com").get("/v1/path/to/foo").reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com", apiVersion: "v1" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - }); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic GET request with a query string", async () => { - nock("https://example.com") - .get("/path/to/foo") - .query({ key: "value" }) - .reply(200, { success: true }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - queryParams: { key: "value" }, - }); - expect(r.body).to.deep.equal({ success: true }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic GET request and not override the user-agent", async () => { - nock("https://example.com") - .get("/path/to/foo") - .matchHeader("user-agent", "unit tests, silly") - .reply(200, { success: true }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - headers: { "user-agent": "unit tests, silly" }, - }); - expect(r.body).to.deep.equal({ success: true }); - expect(nock.isDone()).to.be.true; - }); - - it("should handle a 204 response with no data", async () => { - nock("https://example.com").get("/path/to/foo").reply(204); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - }); - expect(r.body).to.deep.equal(undefined); - expect(nock.isDone()).to.be.true; - }); - - it("should be able to time out if the request takes too long", async () => { - nock("https://example.com").get("/path/to/foo").delay(200).reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com/" }); - await expect( - c.request({ - method: "GET", - path: "/path/to/foo", - timeout: 10, - }) - ).to.eventually.be.rejectedWith(FirebaseError, "Timeout reached making request"); - expect(nock.isDone()).to.be.true; - }); - - it("should be able to be killed by a signal", async () => { - nock("https://example.com").get("/path/to/foo").delay(200).reply(200, { foo: "bar" }); - - const controller = new AbortController(); - setTimeout(() => controller.abort(), 10); - const c = new Client({ urlPrefix: "https://example.com/" }); - await expect( - c.request({ - method: "GET", - path: "/path/to/foo", - signal: controller.signal, - }) - ).to.eventually.be.rejectedWith(FirebaseError, "Timeout reached making request"); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic POST request", async () => { - const POST_DATA = { post: "data" }; - nock("https://example.com") - .matchHeader("Content-Type", "application/json") - .post("/path/to/foo", POST_DATA) - .reply(200, { success: true }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "POST", - path: "/path/to/foo", - body: POST_DATA, - }); - expect(r.body).to.deep.equal({ success: true }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic POST request without overriding Content-Type", async () => { - const POST_DATA = { post: "data" }; - nock("https://example.com") - .matchHeader("Content-Type", "application/json+customcontent") - .post("/path/to/foo", POST_DATA) - .reply(200, { success: true }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "POST", - path: "/path/to/foo", - body: POST_DATA, - headers: { "Content-Type": "application/json+customcontent" }, - }); - expect(r.body).to.deep.equal({ success: true }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a basic POST request with a stream", async () => { - nock("https://example.com").post("/path/to/foo", "hello world").reply(200, { success: true }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.request({ - method: "POST", - path: "/path/to/foo", - body: stringToStream("hello world"), - }); - expect(r.body).to.deep.equal({ success: true }); - expect(nock.isDone()).to.be.true; - }); - - describe("with a proxy", () => { - let proxyServer: Server; - let targetServer: Server; - before(async () => { - proxyServer = proxySetup(createServer()); - targetServer = createServer((req, res) => { - res.writeHead(200, { "content-type": "application/json" }); - res.end(JSON.stringify({ proxied: true })); - }); - await Promise.all([ - new Promise((resolve) => { - proxyServer.listen(52672, resolve); - }), - new Promise((resolve) => { - targetServer.listen(52673, resolve); - }), - ]); - }); - - after(async () => { - await Promise.all([ - new Promise((resolve) => proxyServer.close(resolve)), - new Promise((resolve) => targetServer.close(resolve)), - ]); - }); - - it("should be able to make a basic GET request", async () => { - const c = new Client({ - urlPrefix: "http://127.0.0.1:52673", - proxy: "http://127.0.0.1:52672", - }); - const r = await c.request({ - method: "GET", - path: "/path/to/foo", - }); - expect(r.body).to.deep.equal({ proxied: true }); - expect(nock.isDone()).to.be.true; - }); - }); - }); - - describe("verbs", () => { - it("should make a GET request", async () => { - nock("https://example.com").get("/path/to/foo").reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.get("/path/to/foo"); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a POST request", async () => { - const POST_DATA = { post: "data" }; - nock("https://example.com").post("/path/to/foo", POST_DATA).reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.post("/path/to/foo", POST_DATA); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a PUT request", async () => { - const DATA = { post: "data" }; - nock("https://example.com").put("/path/to/foo", DATA).reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.put("/path/to/foo", DATA); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a PATCH request", async () => { - const DATA = { post: "data" }; - nock("https://example.com").patch("/path/to/foo", DATA).reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.patch("/path/to/foo", DATA); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a DELETE request", async () => { - nock("https://example.com").delete("/path/to/foo").reply(200, { foo: "bar" }); - - const c = new Client({ urlPrefix: "https://example.com" }); - const r = await c.delete("/path/to/foo"); - expect(r.body).to.deep.equal({ foo: "bar" }); - expect(nock.isDone()).to.be.true; - }); - }); -}); diff --git a/src/test/appdistro/client.spec.ts b/src/test/appdistro/client.spec.ts deleted file mode 100644 index d19e5880dca..00000000000 --- a/src/test/appdistro/client.spec.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { expect } from "chai"; -import { join } from "path"; -import * as fs from "fs-extra"; -import * as rimraf from "rimraf"; -import * as sinon from "sinon"; -import * as tmp from "tmp"; - -import { AppDistributionClient, BatchRemoveTestersResponse } from "../../appdistribution/client"; -import { FirebaseError } from "../../error"; -import * as api from "../../api"; -import * as nock from "nock"; -import { Distribution } from "../../appdistribution/distribution"; - -tmp.setGracefulCleanup(); - -describe("distribution", () => { - const tempdir = tmp.dirSync(); - const projectName = "projects/123456789"; - const appName = `${projectName}/apps/1:123456789:ios:abc123def456`; - const binaryFile = join(tempdir.name, "app.ipa"); - fs.ensureFileSync(binaryFile); - const mockDistribution = new Distribution(binaryFile); - const appDistributionClient = new AppDistributionClient(); - - let sandbox: sinon.SinonSandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox.useFakeTimers(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - after(() => { - rimraf.sync(tempdir.name); - }); - - describe("addTesters", () => { - const emails = ["a@foo.com", "b@foo.com"]; - - it("should throw error if request fails", async () => { - nock(api.appDistributionOrigin) - .post(`/v1/${projectName}/testers:batchAdd`) - .reply(400, { error: { status: "FAILED_PRECONDITION" } }); - await expect(appDistributionClient.addTesters(projectName, emails)).to.be.rejectedWith( - FirebaseError, - "Failed to add testers" - ); - expect(nock.isDone()).to.be.true; - }); - - it("should resolve when request succeeds", async () => { - nock(api.appDistributionOrigin).post(`/v1/${projectName}/testers:batchAdd`).reply(200, {}); - await expect(appDistributionClient.addTesters(projectName, emails)).to.be.eventually - .fulfilled; - expect(nock.isDone()).to.be.true; - }); - }); - - describe("deleteTesters", () => { - const emails = ["a@foo.com", "b@foo.com"]; - - it("should throw error if delete fails", async () => { - nock(api.appDistributionOrigin) - .post(`/v1/${projectName}/testers:batchRemove`) - .reply(400, { error: { status: "FAILED_PRECONDITION" } }); - await expect(appDistributionClient.removeTesters(projectName, emails)).to.be.rejectedWith( - FirebaseError, - "Failed to remove testers" - ); - expect(nock.isDone()).to.be.true; - }); - - const mockResponse: BatchRemoveTestersResponse = { emails: emails }; - it("should resolve when request succeeds", async () => { - nock(api.appDistributionOrigin) - .post(`/v1/${projectName}/testers:batchRemove`) - .reply(200, mockResponse); - await expect(appDistributionClient.removeTesters(projectName, emails)).to.eventually.deep.eq( - mockResponse - ); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("uploadRelease", () => { - it("should throw error if upload fails", async () => { - nock(api.appDistributionOrigin).post(`/upload/v1/${appName}/releases:upload`).reply(400, {}); - await expect(appDistributionClient.uploadRelease(appName, mockDistribution)).to.be.rejected; - expect(nock.isDone()).to.be.true; - }); - - it("should return token if upload succeeds", async () => { - const fakeOperation = "fake-operation-name"; - nock(api.appDistributionOrigin) - .post(`/upload/v1/${appName}/releases:upload`) - .reply(200, { name: fakeOperation }); - await expect( - appDistributionClient.uploadRelease(appName, mockDistribution) - ).to.be.eventually.eq(fakeOperation); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("updateReleaseNotes", () => { - const releaseName = `${appName}/releases/fake-release-id`; - it("should return immediately when no release notes are specified", async () => { - const apiSpy = sandbox.spy(api, "request"); - await expect(appDistributionClient.updateReleaseNotes(releaseName, "")).to.eventually.be - .fulfilled; - expect(apiSpy).to.not.be.called; - }); - - it("should throw error when request fails", async () => { - nock(api.appDistributionOrigin) - .patch(`/v1/${releaseName}?updateMask=release_notes.text`) - .reply(400, {}); - await expect( - appDistributionClient.updateReleaseNotes(releaseName, "release notes") - ).to.be.rejectedWith(FirebaseError, "failed to update release notes"); - expect(nock.isDone()).to.be.true; - }); - - it("should resolve when request succeeds", async () => { - nock(api.appDistributionOrigin) - .patch(`/v1/${releaseName}?updateMask=release_notes.text`) - .reply(200, {}); - await expect(appDistributionClient.updateReleaseNotes(releaseName, "release notes")).to - .eventually.be.fulfilled; - expect(nock.isDone()).to.be.true; - }); - }); - - describe("distribute", () => { - const releaseName = `${appName}/releases/fake-release-id`; - it("should return immediately when testers and groups are empty", async () => { - const apiSpy = sandbox.spy(api, "request"); - await expect(appDistributionClient.distribute(releaseName)).to.eventually.be.fulfilled; - expect(apiSpy).to.not.be.called; - }); - - it("should resolve when request succeeds", async () => { - nock(api.appDistributionOrigin).post(`/v1/${releaseName}:distribute`).reply(200, {}); - await expect(appDistributionClient.distribute(releaseName, ["tester1"], ["group1"])).to.be - .fulfilled; - expect(nock.isDone()).to.be.true; - }); - - describe("when request fails", () => { - let testers: string[]; - let groups: string[]; - beforeEach(() => { - testers = ["tester1"]; - groups = ["group1"]; - }); - - it("should throw invalid testers error when status code is FAILED_PRECONDITION ", async () => { - nock(api.appDistributionOrigin) - .post(`/v1/${releaseName}:distribute`, { - testerEmails: testers, - groupAliases: groups, - }) - .reply(412, { error: { status: "FAILED_PRECONDITION" } }); - await expect( - appDistributionClient.distribute(releaseName, testers, groups) - ).to.be.rejectedWith( - FirebaseError, - "failed to distribute to testers/groups: invalid testers" - ); - expect(nock.isDone()).to.be.true; - }); - - it("should throw invalid groups error when status code is INVALID_ARGUMENT", async () => { - nock(api.appDistributionOrigin) - .post(`/v1/${releaseName}:distribute`, { - testerEmails: testers, - groupAliases: groups, - }) - .reply(412, { error: { status: "INVALID_ARGUMENT" } }); - await expect( - appDistributionClient.distribute(releaseName, testers, groups) - ).to.be.rejectedWith( - FirebaseError, - "failed to distribute to testers/groups: invalid groups" - ); - expect(nock.isDone()).to.be.true; - }); - - it("should throw default error", async () => { - nock(api.appDistributionOrigin) - .post(`/v1/${releaseName}:distribute`, { - testerEmails: testers, - groupAliases: groups, - }) - .reply(400, {}); - await expect( - appDistributionClient.distribute(releaseName, ["tester1"], ["group1"]) - ).to.be.rejectedWith(FirebaseError, "failed to distribute to testers/groups"); - expect(nock.isDone()).to.be.true; - }); - }); - }); -}); diff --git a/src/test/archiveDirectory.spec.ts b/src/test/archiveDirectory.spec.ts deleted file mode 100644 index e5771c7a6f7..00000000000 --- a/src/test/archiveDirectory.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve } from "path"; -import { expect } from "chai"; -import { FirebaseError } from "../error"; - -import { archiveDirectory } from "../archiveDirectory"; - -const SOME_FIXTURE_DIRECTORY = resolve(__dirname, "./fixtures/config-imports"); - -describe("archiveDirectory", () => { - it("should archive happy little directories", async () => { - const result = await archiveDirectory(SOME_FIXTURE_DIRECTORY, {}); - expect(result.source).to.equal(SOME_FIXTURE_DIRECTORY); - expect(result.size).to.be.greaterThan(0); - }); - - it("should throw a happy little error if the directory doesn't exist", async () => { - await expect(archiveDirectory(resolve(__dirname, "foo"), {})).to.be.rejectedWith(FirebaseError); - }); -}); diff --git a/src/test/auth.spec.ts b/src/test/auth.spec.ts deleted file mode 100644 index e89a6777e64..00000000000 --- a/src/test/auth.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as auth from "../auth"; -import { configstore } from "../configstore"; - -describe("auth", () => { - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - - let fakeConfigStore: any = {}; - - beforeEach(() => { - const configstoreGetStub = sandbox.stub(configstore, "get"); - configstoreGetStub.callsFake((key: string) => { - return fakeConfigStore[key]; - }); - - const configstoreSetStub = sandbox.stub(configstore, "set"); - configstoreSetStub.callsFake((...values: any) => { - fakeConfigStore[values[0]] = values[1]; - }); - - const configstoreDeleteStub = sandbox.stub(configstore, "delete"); - configstoreDeleteStub.callsFake((key: string) => { - delete fakeConfigStore[key]; - }); - }); - - afterEach(() => { - fakeConfigStore = {}; - sandbox.restore(); - }); - - describe("no accounts", () => { - it("returns no global account when config is empty", () => { - const account = auth.getGlobalDefaultAccount(); - expect(account).to.be.undefined; - }); - }); - - describe("single account", () => { - const defaultAccount: auth.Account = { - user: { - email: "test@test.com", - }, - tokens: { - access_token: "abc1234", - }, - }; - - beforeEach(() => { - configstore.set("user", defaultAccount.user); - configstore.set("tokens", defaultAccount.tokens); - }); - - it("returns global default account", () => { - const account = auth.getGlobalDefaultAccount(); - expect(account).to.deep.equal(defaultAccount); - }); - - it("returns no additional accounts", () => { - const additional = auth.getAdditionalAccounts(); - expect(additional.length).to.equal(0); - }); - - it("returns exactly one total account", () => { - const all = auth.getAllAccounts(); - expect(all.length).to.equal(1); - expect(all[0]).to.deep.equal(defaultAccount); - }); - }); - - describe("multi account", () => { - const defaultAccount: auth.Account = { - user: { - email: "test@test.com", - }, - tokens: { - access_token: "abc1234", - }, - }; - - const additionalUser1: auth.Account = { - user: { - email: "test1@test.com", - }, - tokens: { - access_token: "token1", - }, - }; - - const additionalUser2: auth.Account = { - user: { - email: "test2@test.com", - }, - tokens: { - access_token: "token2", - }, - }; - - const additionalAccounts: auth.Account[] = [additionalUser1, additionalUser2]; - - const activeAccounts = { - "/path/project1": "test1@test.com", - }; - - beforeEach(() => { - configstore.set("user", defaultAccount.user); - configstore.set("tokens", defaultAccount.tokens); - configstore.set("additionalAccounts", additionalAccounts); - configstore.set("activeAccounts", activeAccounts); - }); - - it("returns global default account", () => { - const account = auth.getGlobalDefaultAccount(); - expect(account).to.deep.equal(defaultAccount); - }); - - it("returns additional accounts", () => { - const additional = auth.getAdditionalAccounts(); - expect(additional).to.deep.equal(additionalAccounts); - }); - - it("returns all accounts", () => { - const all = auth.getAllAccounts(); - expect(all).to.deep.equal([defaultAccount, ...additionalAccounts]); - }); - - it("respects project default when present", () => { - const account = auth.getProjectDefaultAccount("/path/project1"); - expect(account).to.deep.equal(additionalUser1); - }); - - it("ignores project default when not present", () => { - const account = auth.getProjectDefaultAccount("/path/project2"); - expect(account).to.deep.equal(defaultAccount); - }); - - it("prefers account flag to project root", () => { - const account = auth.selectAccount("test2@test.com", "/path/project1"); - expect(account).to.deep.equal(additionalUser2); - }); - }); -}); diff --git a/src/test/config.spec.js b/src/test/config.spec.js deleted file mode 100644 index fc6f3a29210..00000000000 --- a/src/test/config.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; - -var chai = require("chai"); -var expect = chai.expect; - -const { Config } = require("../config"); -var path = require("path"); - -var _fixtureDir = function (name) { - return path.resolve(__dirname, "./fixtures/" + name); -}; - -describe("Config", function () { - describe("#load", () => { - it("should load a cjson file when configPath is specified", () => { - const config = Config.load({ - cwd: __dirname, - configPath: "./fixtures/valid-config/firebase.json", - }); - expect(config).to.not.be.null; - if (config) { - expect(config.get("database.rules")).to.eq("config/security-rules.json"); - } - }); - }); - - describe("#parseFile", function () { - it("should load a cjson file", function () { - var config = new Config({}, { cwd: _fixtureDir("config-imports") }); - expect(config.parseFile("hosting", "hosting.json").public).to.equal("."); - }); - - it("should error out for an unknown file", function () { - var config = new Config({}, { cwd: _fixtureDir("config-imports") }); - expect(function () { - config.parseFile("hosting", "i-dont-exist.json"); - }).to.throw("Imported file i-dont-exist.json does not exist"); - }); - - it("should error out for an unrecognized extension", function () { - var config = new Config({}, { cwd: _fixtureDir("config-imports") }); - expect(function () { - config.parseFile("hosting", "unsupported.txt"); - }).to.throw("unsupported.txt is not of a supported config file type"); - }); - }); - - describe("#materialize", function () { - it("should assign unaltered if an object is found", function () { - var config = new Config({ example: { foo: "bar" } }, {}); - expect(config.materialize("example").foo).to.equal("bar"); - }); - - it("should prevent top-level key duplication", function () { - var config = new Config({ rules: "rules.json" }, { cwd: _fixtureDir("dup-top-level") }); - expect(config.materialize("rules")).to.deep.equal({ ".read": true }); - }); - }); -}); diff --git a/src/test/database/api.spec.ts b/src/test/database/api.spec.ts deleted file mode 100644 index c0085814bf4..00000000000 --- a/src/test/database/api.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expect } from "chai"; - -import * as utils from "../../utils"; -import { realtimeOriginOrEmulatorOrCustomUrl, realtimeOriginOrCustomUrl } from "../../database/api"; - -describe("api", () => { - afterEach(() => { - delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; - delete process.env.FIREBASE_REALTIME_URL; - delete process.env.FIREBASE_CLI_PREVIEWS; - // This is dirty, but utils keeps stateful overrides and we need to clear it - utils.envOverrides.length = 0; - }); - - it("should add HTTP to emulator URL with no protocol", () => { - process.env.FIREBASE_DATABASE_EMULATOR_HOST = "localhost:8080"; - expect(realtimeOriginOrEmulatorOrCustomUrl("http://my-custom-url")).to.eq( - "http://localhost:8080" - ); - }); - - it("should not add HTTP to emulator URL with https:// protocol", () => { - process.env.FIREBASE_DATABASE_EMULATOR_HOST = "https://localhost:8080"; - expect(realtimeOriginOrEmulatorOrCustomUrl("http://my-custom-url")).to.eq( - "https://localhost:8080" - ); - }); - - it("should override with FIREBASE_REALTIME_URL", () => { - process.env.FIREBASE_REALTIME_URL = "http://foobar.com"; - expect(realtimeOriginOrEmulatorOrCustomUrl("http://my-custom-url")).to.eq("http://foobar.com"); - }); - - it("should prefer FIREBASE_DATABASE_EMULATOR_HOST to FIREBASE_REALTIME_URL", () => { - process.env.FIREBASE_DATABASE_EMULATOR_HOST = "localhost:8080"; - process.env.FIREBASE_REALTIME_URL = "http://foobar.com"; - expect(realtimeOriginOrEmulatorOrCustomUrl("http://my-custom-url")).to.eq( - "http://localhost:8080" - ); - }); - - it("should prefer FIREBASE_REALTIME_URL when run without emulator", () => { - process.env.FIREBASE_REALTIME_URL = "http://foobar.com"; - expect(realtimeOriginOrCustomUrl("http://my-custom-url")).to.eq("http://foobar.com"); - }); - - it("should ignore FIREBASE_DATABASE_EMULATOR_HOST when run without emulator", () => { - process.env.FIREBASE_DATABASE_EMULATOR_HOST = "localhost:8080"; - process.env.FIREBASE_REALTIME_URL = "http://foobar.com"; - expect(realtimeOriginOrCustomUrl("http://my-custom-url")).to.eq("http://foobar.com"); - }); -}); diff --git a/src/test/database/listRemote.spec.ts b/src/test/database/listRemote.spec.ts deleted file mode 100644 index 9970fbf2ee3..00000000000 --- a/src/test/database/listRemote.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from "chai"; -import * as nock from "nock"; - -import * as utils from "../../utils"; -import { realtimeOrigin } from "../../api"; -import { RTDBListRemote } from "../../database/listRemote"; -const HOST = "https://firebaseio.com"; - -describe("ListRemote", () => { - const instance = "fake-db"; - const remote = new RTDBListRemote(instance, HOST); - const serverUrl = utils.addSubdomain(realtimeOrigin, instance); - - afterEach(() => { - nock.cleanAll(); - }); - - it("should return subpaths from shallow get request", async () => { - nock(serverUrl).get("/.json").query({ shallow: true, limitToFirst: "1234" }).reply(200, { - a: true, - x: true, - f: true, - }); - await expect(remote.listPath("/", 1234)).to.eventually.eql(["a", "x", "f"]); - }); -}); diff --git a/src/test/database/removeRemote.spec.ts b/src/test/database/removeRemote.spec.ts deleted file mode 100644 index 869c6a741dd..00000000000 --- a/src/test/database/removeRemote.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { expect } from "chai"; -import * as nock from "nock"; - -import * as utils from "../../utils"; -import { RTDBRemoveRemote } from "../../database/removeRemote"; - -describe("RemoveRemote", () => { - const instance = "fake-db"; - const host = "https://firebaseio.com"; - const remote = new RTDBRemoveRemote(instance, host); - const serverUrl = utils.getDatabaseUrl(host, instance, ""); - - afterEach(() => { - nock.cleanAll(); - }); - - it("should return true when patch is small", () => { - nock(serverUrl) - .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) - .reply(200, {}); - return expect(remote.deletePath("/a/b")).to.eventually.eql(true); - }); - - it("should return false whem patch is large", () => { - nock(serverUrl) - .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) - .reply(400, { - error: - "Data requested exceeds the maximum size that can be accessed with a single request.", - }); - return expect(remote.deleteSubPath("/a/b", ["1", "2", "3"])).to.eventually.eql(false); - }); - - it("should return true when multi-path patch is small", () => { - nock(serverUrl) - .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) - .reply(200, {}); - return expect(remote.deleteSubPath("/a/b", ["1", "2", "3"])).to.eventually.eql(true); - }); - - it("should return false when multi-path patch is large", () => { - nock(serverUrl) - .patch("/a/b.json") - .query({ print: "silent", writeSizeLimit: "tiny" }) - .reply(400, { - error: - "Data requested exceeds the maximum size that can be accessed with a single request.", - }); - return expect(remote.deleteSubPath("/a/b", ["1", "2", "3"])).to.eventually.eql(false); - }); -}); diff --git a/src/test/deploy/extensions/params.spec.ts b/src/test/deploy/extensions/params.spec.ts deleted file mode 100644 index 02f1f7869d7..00000000000 --- a/src/test/deploy/extensions/params.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as params from "../../../deploy/extensions/params"; -import * as paramHelper from "../../../extensions/paramHelper"; - -describe("readParams", () => { - let readEnvFileStub: sinon.SinonStub; - const testProjectDir = "test"; - const testProjectId = "my-project"; - const testProjectNumber = "123456"; - const testInstanceId = "extensionId"; - - beforeEach(() => { - readEnvFileStub = sinon.stub(paramHelper, "readEnvFile").returns({}); - }); - - afterEach(() => { - readEnvFileStub.restore(); - }); - - it("should read from generic .env file", () => { - readEnvFileStub - .withArgs("test/extensions/extensionId.env") - .returns({ param: "otherValue", param2: "value2" }); - - expect( - params.readParams({ - projectDir: testProjectDir, - instanceId: testInstanceId, - projectId: testProjectId, - projectNumber: testProjectNumber, - aliases: [], - }) - ).to.deep.equal({ param: "otherValue", param2: "value2" }); - }); - - it("should read from project id .env file", () => { - readEnvFileStub - .withArgs("test/extensions/extensionId.env.my-project") - .returns({ param: "otherValue", param2: "value2" }); - - expect( - params.readParams({ - projectDir: testProjectDir, - instanceId: testInstanceId, - projectId: testProjectId, - projectNumber: testProjectNumber, - aliases: [], - }) - ).to.deep.equal({ param: "otherValue", param2: "value2" }); - }); - - it("should read from project number .env file", () => { - readEnvFileStub - .withArgs("test/extensions/extensionId.env.123456") - .returns({ param: "otherValue", param2: "value2" }); - - expect( - params.readParams({ - projectDir: testProjectDir, - instanceId: testInstanceId, - projectId: testProjectId, - projectNumber: testProjectNumber, - aliases: [], - }) - ).to.deep.equal({ param: "otherValue", param2: "value2" }); - }); - - it("should read from an alias .env file", () => { - readEnvFileStub - .withArgs("test/extensions/extensionId.env.prod") - .returns({ param: "otherValue", param2: "value2" }); - - expect( - params.readParams({ - projectDir: testProjectDir, - instanceId: testInstanceId, - projectId: testProjectId, - projectNumber: testProjectNumber, - aliases: ["prod"], - }) - ).to.deep.equal({ param: "otherValue", param2: "value2" }); - }); - - it("should prefer values from project specific env files", () => { - readEnvFileStub - .withArgs("test/extensions/extensionId.env.my-project") - .returns({ param: "value" }); - readEnvFileStub - .withArgs("test/extensions/extensionId.env") - .returns({ param: "otherValue", param2: "value2" }); - - expect( - params.readParams({ - projectDir: testProjectDir, - instanceId: testInstanceId, - projectId: testProjectId, - projectNumber: testProjectNumber, - aliases: [], - }) - ).to.deep.equal({ param: "value", param2: "value2" }); - }); -}); diff --git a/src/test/deploy/extensions/planner.spec.ts b/src/test/deploy/extensions/planner.spec.ts deleted file mode 100644 index 77a2aaa6dfa..00000000000 --- a/src/test/deploy/extensions/planner.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as planner from "../../../deploy/extensions/planner"; -import * as extensionsApi from "../../../extensions/extensionsApi"; - -function extensionVersion(version: string): extensionsApi.ExtensionVersion { - return { - name: `publishers/test/extensions/test/versions/${version}`, - ref: `test/test@${version}`, - state: "PUBLISHED", - hash: "abc123", - sourceDownloadUri: "https://google.com", - spec: { - name: "test", - version, - resources: [], - sourceUrl: "https://google.com", - params: [], - }, - }; -} -describe("Extensions Deployment Planner", () => { - describe("resolveSemver", () => { - let listExtensionVersionsStub: sinon.SinonStub; - - before(() => { - listExtensionVersionsStub = sinon - .stub(extensionsApi, "listExtensionVersions") - .resolves([ - extensionVersion("0.1.0"), - extensionVersion("0.1.1"), - extensionVersion("0.2.0"), - ]); - }); - - after(() => { - listExtensionVersionsStub.restore(); - }); - - const cases = [ - { - description: "should return the latest version that satisifies a semver range", - in: "^0.1.0", - out: "0.1.1", - err: false, - }, - { - description: "should match exact semver", - in: "0.2.0", - out: "0.2.0", - err: false, - }, - { - description: "should allow latest", - in: "latest", - out: "latest", - err: false, - }, - { - description: "should default to latest", - out: "latest", - err: false, - }, - { - description: "should error if there is no matching version", - in: "^0.3.0", - err: true, - }, - ]; - - for (const c of cases) { - it(c.description, () => { - if (!c.err) { - expect( - planner.resolveVersion({ - publisherId: "test", - extensionId: "test", - version: c.in, - }) - ).to.eventually.equal(c.out); - } else { - expect( - planner.resolveVersion({ - publisherId: "test", - extensionId: "test", - version: c.in, - }) - ).to.eventually.be.rejected; - } - }); - } - }); -}); diff --git a/src/test/deploy/functions/checkIam.spec.ts b/src/test/deploy/functions/checkIam.spec.ts deleted file mode 100644 index 2c4da1473fc..00000000000 --- a/src/test/deploy/functions/checkIam.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as checkIam from "../../../deploy/functions/checkIam"; -import * as storage from "../../../gcp/storage"; -import * as rm from "../../../gcp/resourceManager"; -import * as backend from "../../../deploy/functions/backend"; - -const STORAGE_RES = { - email_address: "service-123@gs-project-accounts.iam.gserviceaccount.com", - kind: "storage#serviceAccount", -}; - -const BINDING = { - role: "some/role", - members: ["someuser"], -}; - -const SPEC = { - region: "us-west1", - project: "my-project", - runtime: "nodejs14", -}; - -describe("checkIam", () => { - let storageStub: sinon.SinonStub; - let getIamStub: sinon.SinonStub; - let setIamStub: sinon.SinonStub; - - beforeEach(() => { - storageStub = sinon - .stub(storage, "getServiceAccount") - .throws("unexpected call to storage.getServiceAccount"); - getIamStub = sinon - .stub(rm, "getIamPolicy") - .throws("unexpected call to resourceManager.getIamStub"); - setIamStub = sinon - .stub(rm, "setIamPolicy") - .throws("unexpected call to resourceManager.setIamPolicy"); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - describe("mergeBindings", () => { - it("should skip empty or duplicate bindings", () => { - const policy = { - etag: "etag", - version: 3, - bindings: [BINDING], - }; - - checkIam.mergeBindings(policy, [[], [BINDING]]); - - expect(policy.bindings).to.deep.equal([BINDING]); - }); - - it("should update current binding", () => { - const policy = { - etag: "etag", - version: 3, - bindings: [BINDING], - }; - - checkIam.mergeBindings(policy, [[{ role: "some/role", members: ["newuser"] }]]); - - expect(policy.bindings).to.deep.equal([ - { - role: "some/role", - members: ["someuser", "newuser"], - }, - ]); - }); - - it("should add the binding", () => { - const policy = { - etag: "etag", - version: 3, - bindings: [], - }; - - checkIam.mergeBindings(policy, [[BINDING]]); - - expect(policy.bindings).to.deep.equal([BINDING]); - }); - }); - - describe("ensureServiceAgentRoles", () => { - it("should return early if we fail to get the IAM policy", async () => { - getIamStub.rejects("Failed to get the IAM policy"); - const wantFn: backend.Endpoint = { - id: "wantFn", - entryPoint: "wantFn", - platform: "gcfv2", - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalized", - eventFilters: { - bucket: "my-bucket", - }, - retry: false, - }, - ...SPEC, - }; - - await expect(checkIam.ensureServiceAgentRoles("project", backend.of(wantFn), backend.empty())) - .to.not.be.rejected; - expect(getIamStub).to.have.been.calledOnce; - expect(getIamStub).to.have.been.calledWith("project"); - expect(storageStub).to.not.have.been.called; - expect(setIamStub).to.not.have.been.called; - }); - - it("should skip v1, callable, and deployed functions", async () => { - const v1EventFn: backend.Endpoint = { - id: "v1eventfn", - entryPoint: "v1Fn", - platform: "gcfv1", - eventTrigger: { - eventType: "google.storage.object.create", - eventFilters: { - resource: "projects/_/buckets/myBucket", - }, - retry: false, - }, - ...SPEC, - }; - const v2CallableFn: backend.Endpoint = { - id: "v2callablefn", - entryPoint: "v2callablefn", - platform: "gcfv2", - httpsTrigger: {}, - ...SPEC, - }; - const wantFn: backend.Endpoint = { - id: "wantFn", - entryPoint: "wantFn", - platform: "gcfv2", - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalized", - eventFilters: { - bucket: "my-bucket", - }, - retry: false, - }, - ...SPEC, - }; - - await checkIam.ensureServiceAgentRoles( - "project", - backend.of(wantFn), - backend.of(v1EventFn, v2CallableFn, wantFn) - ); - - expect(storageStub).to.not.have.been.called; - expect(getIamStub).to.not.have.been.called; - expect(setIamStub).to.not.have.been.called; - }); - - it("should skip if we have a deployed event fn of the same kind", async () => { - const wantFn: backend.Endpoint = { - id: "wantFn", - entryPoint: "wantFn", - platform: "gcfv2", - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalized", - eventFilters: { - bucket: "my-bucket", - }, - retry: false, - }, - ...SPEC, - }; - const haveFn: backend.Endpoint = { - id: "haveFn", - entryPoint: "haveFn", - platform: "gcfv2", - eventTrigger: { - eventType: "google.cloud.storage.object.v1.metadataUpdated", - eventFilters: { - bucket: "my-bucket", - }, - retry: false, - }, - ...SPEC, - }; - - await checkIam.ensureServiceAgentRoles("project", backend.of(wantFn), backend.of(haveFn)); - - expect(storageStub).to.not.have.been.called; - expect(getIamStub).to.not.have.been.called; - expect(setIamStub).to.not.have.been.called; - }); - - it("should add the binding with the service agent", async () => { - const newIamPolicy = { - etag: "etag", - version: 3, - bindings: [ - BINDING, - { - role: "roles/pubsub.publisher", - members: [`serviceAccount:${STORAGE_RES.email_address}`], - }, - ], - }; - storageStub.resolves(STORAGE_RES); - getIamStub.resolves({ - etag: "etag", - version: 3, - bindings: [BINDING], - }); - setIamStub.resolves(newIamPolicy); - const wantFn: backend.Endpoint = { - id: "wantFn", - entryPoint: "wantFn", - platform: "gcfv2", - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalized", - eventFilters: { - bucket: "my-bucket", - }, - retry: false, - }, - ...SPEC, - }; - - await checkIam.ensureServiceAgentRoles("project", backend.of(wantFn), backend.empty()); - - expect(storageStub).to.have.been.calledOnce; - expect(getIamStub).to.have.been.calledOnce; - expect(setIamStub).to.have.been.calledOnce; - expect(setIamStub).to.have.been.calledWith("project", newIamPolicy, "bindings"); - }); - }); -}); diff --git a/src/test/deploy/functions/ensureCloudBuildEnabled.ts b/src/test/deploy/functions/ensureCloudBuildEnabled.ts deleted file mode 100644 index 8f50ece8794..00000000000 --- a/src/test/deploy/functions/ensureCloudBuildEnabled.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as nock from "nock"; - -import { FirebaseError } from "../../../error"; -import { logger } from "../../../logger"; -import { configstore } from "../../../configstore"; -import { ensureCloudBuildEnabled } from "../../../deploy/functions/ensureCloudBuildEnabled"; -import { POLL_SETTINGS } from "../../../ensureApiEnabled"; -import * as api from "../../../api"; - -describe("ensureCloudBuildEnabled()", () => { - let restoreInterval: number; - before(() => { - restoreInterval = POLL_SETTINGS.pollInterval; - POLL_SETTINGS.pollInterval = 0; - }); - after(() => { - POLL_SETTINGS.pollInterval = restoreInterval; - }); - - let sandbox: sinon.SinonSandbox; - let logStub: sinon.SinonStub | null; - beforeEach(() => { - sandbox = sinon.createSandbox(); - logStub = sandbox.stub(logger, "warn"); - }); - - afterEach(() => { - expect(nock.isDone()).to.be.true; - sandbox.restore(); - timeStub = null; - logStub = null; - }); - - function mockServiceCheck(isEnabled = false): void { - nock(api.serviceUsageOrigin) - .get("/v1/projects/test-project/services/cloudbuild.googleapis.com") - .reply(200, { state: isEnabled ? "ENABLED" : "DISABLED" }); - } - - function mockServiceEnableSuccess(): void { - nock(api.serviceUsageOrigin) - .post("/v1/projects/test-project/services/cloudbuild.googleapis.com:enable") - .reply(200, {}); - } - - function mockServiceEnableBillingError(): void { - nock(api.serviceUsageOrigin) - .post("/v1/projects/test-project/services/cloudbuild.googleapis.com:enable") - .reply(403, { - error: { - details: [{ violations: [{ type: "serviceusage/billing-enabled" }] }], - }, - }); - } - - function mockServiceEnablePermissionError(): void { - nock(api.serviceUsageOrigin) - .post("/v1/projects/test-project/services/cloudbuild.googleapis.com:enable") - .reply(403, { - error: { - status: "PERMISSION_DENIED", - }, - }); - } - - let timeStub: sinon.SinonStub | null; - function stubTimes(warnAfter: number, errorAfter: number): void { - timeStub = sandbox.stub(configstore, "get"); - timeStub.withArgs("motd.cloudBuildWarnAfter").returns(warnAfter); - timeStub.withArgs("motd.cloudBuildErrorAfter").returns(errorAfter); - } - - describe("with cloudbuild service enabled", () => { - beforeEach(() => { - mockServiceCheck(true); - }); - - it("should succeed", async () => { - stubTimes(Date.now() - 10000, Date.now() - 5000); - - await expect(ensureCloudBuildEnabled("test-project")).to.eventually.be.fulfilled; - expect(logStub?.callCount).to.eq(0); - }); - }); - - describe("with cloudbuild service disabled, but enabling succeeds", () => { - beforeEach(() => { - mockServiceCheck(false); - mockServiceEnableSuccess(); - mockServiceCheck(true); - }); - - it("should succeed", async () => { - stubTimes(Date.now() - 10000, Date.now() - 5000); - - await expect(ensureCloudBuildEnabled("test-project")).to.eventually.be.fulfilled; - expect(logStub?.callCount).to.eq(1); // enabling an api logs a warning - }); - }); - - describe("with cloudbuild service disabled, but enabling fails with billing error", () => { - beforeEach(() => { - mockServiceCheck(false); - mockServiceEnableBillingError(); - }); - - it("should error", async () => { - stubTimes(Date.now() - 10000, Date.now() - 5000); - - await expect(ensureCloudBuildEnabled("test-project")).to.eventually.be.rejectedWith( - FirebaseError, - /must be on the Blaze \(pay-as-you-go\) plan to complete this command/ - ); - }); - }); - - describe("with cloudbuild service disabled, but enabling fails with permission error", () => { - beforeEach(() => { - mockServiceCheck(false); - mockServiceEnablePermissionError(); - }); - - it("should error", async () => { - stubTimes(Date.now() - 10000, Date.now() - 5000); - - await expect(ensureCloudBuildEnabled("test-project")).to.eventually.be.rejectedWith( - FirebaseError, - /Please ask a project owner to visit the following URL to enable Cloud Build/ - ); - }); - }); -}); diff --git a/src/test/deploy/functions/functionsDeployHelper.spec.ts b/src/test/deploy/functions/functionsDeployHelper.spec.ts deleted file mode 100644 index 469ff205884..00000000000 --- a/src/test/deploy/functions/functionsDeployHelper.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { expect } from "chai"; - -import * as backend from "../../../deploy/functions/backend"; -import * as helper from "../../../deploy/functions/functionsDeployHelper"; -import { Options } from "../../../options"; - -describe("functionsDeployHelper", () => { - const ENDPOINT: Omit = { - platform: "gcfv1", - project: "project", - region: "us-central1", - runtime: "nodejs16", - entryPoint: "function", - }; - - describe("functionMatchesGroup", () => { - it("should match empty filters", () => { - const func = { ...ENDPOINT, id: "id" }; - expect(helper.functionMatchesGroup(func, [])).to.be.true; - }); - - it("should match full names", () => { - const func = { ...ENDPOINT, id: "id" }; - expect(helper.functionMatchesGroup(func, ["id"])).to.be.true; - }); - - it("should match group prefixes", () => { - const func = { ...ENDPOINT, id: "group-subgroup-func" }; - expect(helper.functionMatchesGroup(func, ["group", "subgroup", "func"])).to.be.true; - expect(helper.functionMatchesGroup(func, ["group", "subgroup"])).to.be.true; - expect(helper.functionMatchesGroup(func, ["group"])).to.be.true; - }); - - it("should exclude functions that don't match", () => { - const func = { ...ENDPOINT, id: "id" }; - expect(helper.functionMatchesGroup(func, ["group"])).to.be.false; - }); - }); - - describe("functionMatchesAnyGroup", () => { - it("should match empty filters", () => { - const func = { ...ENDPOINT, id: "id" }; - expect(helper.functionMatchesAnyGroup(func, [[]])).to.be.true; - }); - - it("should match against one filter", () => { - const func = { ...ENDPOINT, id: "id" }; - expect(helper.functionMatchesAnyGroup(func, [["id"], ["group"]])).to.be.true; - }); - - it("should exclude functions that don't match", () => { - const func = { ...ENDPOINT, id: "id" }; - expect(helper.functionMatchesAnyGroup(func, [["group"], ["other-group"]])).to.be.false; - }); - }); - - describe("getFilterGroups", () => { - it("should parse multiple filters", () => { - const options = { - only: "functions:myFunc,functions:myOtherFunc", - } as Options; - expect(helper.getFilterGroups(options)).to.deep.equal([["myFunc"], ["myOtherFunc"]]); - }); - - it("should parse nested filters", () => { - const options = { - only: "functions:groupA.myFunc", - } as Options; - expect(helper.getFilterGroups(options)).to.deep.equal([["groupA", "myFunc"]]); - }); - }); -}); diff --git a/src/test/deploy/functions/prepare.spec.ts b/src/test/deploy/functions/prepare.spec.ts deleted file mode 100644 index b8a15ab946c..00000000000 --- a/src/test/deploy/functions/prepare.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { expect } from "chai"; - -import * as backend from "../../../deploy/functions/backend"; -import * as prepare from "../../../deploy/functions/prepare"; - -describe("prepare", () => { - describe("inferDetailsFromExisting", () => { - const ENDPOINT_BASE: Omit = { - platform: "gcfv2", - id: "id", - region: "region", - project: "project", - entryPoint: "entry", - runtime: "nodejs16", - }; - - const ENDPOINT: backend.Endpoint = { - ...ENDPOINT_BASE, - httpsTrigger: {}, - }; - - it("merges env vars if .env is not used", () => { - const oldE = { - ...ENDPOINT, - environmentVariables: { - foo: "old value", - old: "value", - }, - }; - const newE = { - ...ENDPOINT, - environmentVariables: { - foo: "new value", - new: "value", - }, - }; - - prepare.inferDetailsFromExisting(backend.of(newE), backend.of(oldE), /* usedDotenv= */ false); - - expect(newE.environmentVariables).to.deep.equals({ - old: "value", - new: "value", - foo: "new value", - }); - }); - - it("overwrites env vars if .env is used", () => { - const oldE = { - ...ENDPOINT, - environmentVariables: { - foo: "old value", - old: "value", - }, - }; - const newE = { - ...ENDPOINT, - environmentVariables: { - foo: "new value", - new: "value", - }, - }; - - prepare.inferDetailsFromExisting(backend.of(newE), backend.of(oldE), /* usedDotEnv= */ true); - - expect(newE.environmentVariables).to.deep.equals({ - new: "value", - foo: "new value", - }); - }); - - it("can noop when there is no prior endpoint", () => { - const e = { ...ENDPOINT }; - prepare.inferDetailsFromExisting(backend.of(e), backend.of(), /* usedDotEnv= */ false); - expect(e).to.deep.equal(ENDPOINT); - }); - - it("can fill in regions from last deploy", () => { - const want: backend.Endpoint = { - ...ENDPOINT_BASE, - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalized", - eventFilters: { - bucket: "bucket", - }, - retry: false, - }, - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const have: backend.Endpoint & backend.EventTriggered = JSON.parse(JSON.stringify(want)); - have.eventTrigger.region = "us"; - - prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* usedDotEnv= */ false); - expect(want.eventTrigger.region).to.equal("us"); - }); - - it("doesn't fill in regions if triggers changed", () => { - const want: backend.Endpoint = { - ...ENDPOINT_BASE, - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalzied", - eventFilters: { - bucket: "us-bucket", - }, - retry: false, - }, - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const have: backend.Endpoint & backend.EventTriggered = JSON.parse(JSON.stringify(want)); - have.eventTrigger.eventFilters["bucket"] = "us-central1-bucket"; - have.eventTrigger.region = "us-central1"; - - prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* usedDotEnv= */ false); - expect(want.eventTrigger.region).to.be.undefined; - }); - - it("fills in instance size", () => { - const want: backend.Endpoint = { - ...ENDPOINT_BASE, - httpsTrigger: {}, - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const have: backend.Endpoint = JSON.parse(JSON.stringify(want)); - have.availableMemoryMb = 512; - - prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* usedDotEnv= */ false); - expect(want.availableMemoryMb).to.equal(512); - }); - }); -}); diff --git a/src/test/deploy/functions/prompts.spec.ts b/src/test/deploy/functions/prompts.spec.ts deleted file mode 100644 index eb25cf5f4dc..00000000000 --- a/src/test/deploy/functions/prompts.spec.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../../error"; -import * as backend from "../../../deploy/functions/backend"; -import * as functionPrompts from "../../../deploy/functions/prompts"; -import * as prompt from "../../../prompt"; -import * as utils from "../../../utils"; -import { Options } from "../../../options"; -import { RC } from "../../../rc"; - -const SAMPLE_EVENT_TRIGGER: backend.EventTrigger = { - eventType: "google.pubsub.topic.publish", - eventFilters: { - resource: "projects/a/topics/b", - }, - retry: false, -}; - -const SAMPLE_ENDPOINT: backend.Endpoint = { - platform: "gcfv1", - id: "c", - region: "us-central1", - project: "a", - entryPoint: "function", - labels: {}, - environmentVariables: {}, - runtime: "nodejs16", - eventTrigger: SAMPLE_EVENT_TRIGGER, -}; - -const SAMPLE_OPTIONS: Options = { - cwd: "/", - configPath: "/", - /* eslint-disable-next-line */ - config: {} as any, - only: "functions", - except: "", - nonInteractive: false, - json: false, - interactive: false, - debug: false, - force: false, - filteredTargets: ["functions"], - rc: new RC(), -}; - -describe("promptForFailurePolicies", () => { - let promptStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - }); - - afterEach(() => { - promptStub.restore(); - }); - - it("should prompt if there are new functions with failure policies", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - retry: true, - }, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForFailurePolicies( - SAMPLE_OPTIONS, - backend.of(endpoint), - backend.empty() - ) - ).not.to.be.rejected; - expect(promptStub).to.have.been.calledOnce; - }); - - it("should not prompt if all functions with failure policies already had failure policies", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - retry: true, - }, - }; - - await expect( - functionPrompts.promptForFailurePolicies( - SAMPLE_OPTIONS, - backend.of(endpoint), - backend.of(endpoint) - ) - ).eventually.be.fulfilled; - expect(promptStub).to.not.have.been.called; - }); - - it("should throw if user declines the prompt", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - retry: true, - }, - }; - promptStub.resolves(false); - - await expect( - functionPrompts.promptForFailurePolicies( - SAMPLE_OPTIONS, - backend.of(endpoint), - backend.empty() - ) - ).to.eventually.be.rejectedWith(FirebaseError, /Deployment canceled/); - expect(promptStub).to.have.been.calledOnce; - }); - - it("should prompt if an existing function adds a failure policy", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - }, - }; - const newEndpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - retry: true, - }, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForFailurePolicies( - SAMPLE_OPTIONS, - backend.of(newEndpoint), - backend.of(endpoint) - ) - ).eventually.be.fulfilled; - expect(promptStub).to.have.been.calledOnce; - }); - - it("should throw if there are any functions with failure policies and the user doesn't accept the prompt", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - retry: true, - }, - }; - promptStub.resolves(false); - - await expect( - functionPrompts.promptForFailurePolicies( - SAMPLE_OPTIONS, - backend.of(endpoint), - backend.empty() - ) - ).to.eventually.be.rejectedWith(FirebaseError, /Deployment canceled/); - expect(promptStub).to.have.been.calledOnce; - }); - - it("should not prompt if there are no functions with failure policies", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - }, - }; - promptStub.resolves(); - - await expect( - functionPrompts.promptForFailurePolicies( - SAMPLE_OPTIONS, - backend.of(endpoint), - backend.empty() - ) - ).to.eventually.be.fulfilled; - expect(promptStub).not.to.have.been.called; - }); - - it("should throw if there are any functions with failure policies, in noninteractive mode, without the force flag set", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - retry: true, - }, - }; - const options = { ...SAMPLE_OPTIONS, nonInteractive: true }; - - await expect( - functionPrompts.promptForFailurePolicies(options, backend.of(endpoint), backend.empty()) - ).to.be.rejectedWith(FirebaseError, /--force option/); - expect(promptStub).not.to.have.been.called; - }); - - it("should not throw if there are any functions with failure policies, in noninteractive mode, with the force flag set", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - eventTrigger: { - ...SAMPLE_EVENT_TRIGGER, - retry: true, - }, - }; - const options = { ...SAMPLE_OPTIONS, nonInteractive: true, force: true }; - - await expect( - functionPrompts.promptForFailurePolicies(options, backend.of(endpoint), backend.empty()) - ).to.eventually.be.fulfilled; - expect(promptStub).not.to.have.been.called; - }); -}); - -describe("promptForMinInstances", () => { - let promptStub: sinon.SinonStub; - let logStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - logStub = sinon.stub(utils, "logLabeledWarning"); - }); - - afterEach(() => { - promptStub.restore(); - logStub.restore(); - }); - - it("should prompt if there are new functions with minInstances", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 1, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForMinInstances(SAMPLE_OPTIONS, backend.of(endpoint), backend.empty()) - ).not.to.be.rejected; - expect(promptStub).to.have.been.calledOnce; - }); - - it("should not prompt if no fucntion has minInstance", async () => { - const bkend = backend.of(SAMPLE_ENDPOINT); - await expect(functionPrompts.promptForMinInstances(SAMPLE_OPTIONS, bkend, bkend)).to.eventually - .be.fulfilled; - expect(promptStub).to.not.have.been.called; - }); - - it("should not prompt if all functions with minInstances already had the same number of minInstances", async () => { - const bkend = backend.of({ - ...SAMPLE_ENDPOINT, - minInstances: 1, - }); - - await expect(functionPrompts.promptForMinInstances(SAMPLE_OPTIONS, bkend, bkend)).to.eventually - .be.fulfilled; - expect(promptStub).to.not.have.been.called; - }); - - it("should not prompt if functions decrease in minInstances", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 2, - }; - const newEndpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 1, - }; - - await expect( - functionPrompts.promptForMinInstances( - SAMPLE_OPTIONS, - backend.of(newEndpoint), - backend.of(endpoint) - ) - ).eventually.be.fulfilled; - expect(promptStub).to.not.have.been.called; - }); - - it("should throw if user declines the prompt", async () => { - const bkend = backend.of({ - ...SAMPLE_ENDPOINT, - minInstances: 1, - }); - promptStub.resolves(false); - await expect( - functionPrompts.promptForMinInstances(SAMPLE_OPTIONS, bkend, backend.empty()) - ).to.eventually.be.rejectedWith(FirebaseError, /Deployment canceled/); - expect(promptStub).to.have.been.calledOnce; - }); - - it("should prompt if an existing function sets minInstances", async () => { - const newEndpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 1, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForMinInstances( - SAMPLE_OPTIONS, - backend.of(newEndpoint), - backend.of(SAMPLE_ENDPOINT) - ) - ).eventually.be.fulfilled; - expect(promptStub).to.have.been.calledOnce; - }); - - it("should prompt if an existing function increases minInstances", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 1, - }; - const newEndpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 2, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForMinInstances( - SAMPLE_OPTIONS, - backend.of(newEndpoint), - backend.of(endpoint) - ) - ).eventually.be.fulfilled; - expect(promptStub).to.have.been.calledOnce; - }); - - it("should prompt if a minInstance function increases resource reservations", async () => { - const endpoint: backend.Endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 2, - availableMemoryMb: 1024, - }; - const newEndpoint: backend.Endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 2, - availableMemoryMb: 2048, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForMinInstances( - SAMPLE_OPTIONS, - backend.of(newEndpoint), - backend.of(endpoint) - ) - ).eventually.be.fulfilled; - expect(promptStub).to.have.been.calledOnce; - }); - - it("should throw if there are any functions with failure policies and the user doesn't accept the prompt", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 2, - }; - promptStub.resolves(false); - - await expect( - functionPrompts.promptForMinInstances(SAMPLE_OPTIONS, backend.of(endpoint), backend.empty()) - ).to.eventually.be.rejectedWith(FirebaseError, /Deployment canceled/); - expect(promptStub).to.have.been.calledOnce; - }); - - it("should not prompt if there are no functions with minInstances", async () => { - promptStub.resolves(); - - await expect( - functionPrompts.promptForMinInstances( - SAMPLE_OPTIONS, - backend.of(SAMPLE_ENDPOINT), - backend.empty() - ) - ).to.eventually.be.fulfilled; - expect(promptStub).not.to.have.been.called; - }); - - it("should throw if there are any functions with minInstances, in noninteractive mode, without the force flag set", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 1, - }; - const options = { ...SAMPLE_OPTIONS, nonInteractive: true }; - - await expect( - functionPrompts.promptForMinInstances(options, backend.of(endpoint), backend.empty()) - ).to.be.rejectedWith(FirebaseError, /--force option/); - expect(promptStub).not.to.have.been.called; - }); - - it("should not throw if there are any functions with minInstances, in noninteractive mode, with the force flag set", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - minInstances: 1, - }; - const options = { ...SAMPLE_OPTIONS, nonInteractive: true, force: true }; - - await expect( - functionPrompts.promptForMinInstances(options, backend.of(endpoint), backend.empty()) - ).to.eventually.be.fulfilled; - expect(promptStub).not.to.have.been.called; - }); - - it("Should disclaim if a bill cannot be calculated", async () => { - const endpoint = { - ...SAMPLE_ENDPOINT, - region: "fillory", - minInstances: 1, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForMinInstances(SAMPLE_OPTIONS, backend.of(endpoint), backend.empty()) - ).to.eventually.be.fulfilled; - expect(promptStub).to.have.been.called; - expect(logStub.firstCall.args[1]).to.match(/Cannot calculate the minimum monthly bill/); - }); - - it("Should advise customers of possible discounts", async () => { - const endpoint: backend.Endpoint = { - ...SAMPLE_ENDPOINT, - platform: "gcfv2", - minInstances: 2, - }; - promptStub.resolves(true); - - await expect( - functionPrompts.promptForMinInstances(SAMPLE_OPTIONS, backend.of(endpoint), backend.empty()) - ).to.eventually.be.fulfilled; - expect(promptStub).to.have.been.called; - expect(logStub.firstCall.args[1]).to.match(new RegExp("https://cloud.google.com/run/cud")); - }); -}); diff --git a/src/test/deploy/functions/release/fabricator.spec.ts b/src/test/deploy/functions/release/fabricator.spec.ts deleted file mode 100644 index 1dcf479cff4..00000000000 --- a/src/test/deploy/functions/release/fabricator.spec.ts +++ /dev/null @@ -1,1082 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as fabricator from "../../../../deploy/functions/release/fabricator"; -import * as reporter from "../../../../deploy/functions/release/reporter"; -import * as executor from "../../../../deploy/functions/release/executor"; -import * as gcfNSV2 from "../../../../gcp/cloudfunctionsv2"; -import * as gcfNS from "../../../../gcp/cloudfunctions"; -import * as pollerNS from "../../../../operation-poller"; -import * as pubsubNS from "../../../../gcp/pubsub"; -import * as schedulerNS from "../../../../gcp/cloudscheduler"; -import * as runNS from "../../../../gcp/run"; -import * as cloudtasksNS from "../../../../gcp/cloudtasks"; -import * as backend from "../../../../deploy/functions/backend"; -import * as scraper from "../../../../deploy/functions/release/sourceTokenScraper"; -import * as planner from "../../../../deploy/functions/release/planner"; - -describe("Fabricator", () => { - // Stub all GCP APIs to make sure this test is hermetic - let gcf: sinon.SinonStubbedInstance; - let gcfv2: sinon.SinonStubbedInstance; - let poller: sinon.SinonStubbedInstance; - let pubsub: sinon.SinonStubbedInstance; - let scheduler: sinon.SinonStubbedInstance; - let run: sinon.SinonStubbedInstance; - let tasks: sinon.SinonStubbedInstance; - - beforeEach(() => { - gcf = sinon.stub(gcfNS); - gcfv2 = sinon.stub(gcfNSV2); - poller = sinon.stub(pollerNS); - pubsub = sinon.stub(pubsubNS); - scheduler = sinon.stub(schedulerNS); - run = sinon.stub(runNS); - tasks = sinon.stub(cloudtasksNS); - - gcf.functionFromEndpoint.restore(); - gcfv2.functionFromEndpoint.restore(); - scheduler.jobFromEndpoint.restore(); - tasks.queueFromEndpoint.restore(); - tasks.queueNameForEndpoint.restore(); - gcf.createFunction.rejects(new Error("unexpected gcf.createFunction")); - gcf.updateFunction.rejects(new Error("unexpected gcf.updateFunction")); - gcf.deleteFunction.rejects(new Error("unexpected gcf.deleteFunction")); - gcf.getIamPolicy.rejects(new Error("unexpected gcf.getIamPolicy")); - gcf.setIamPolicy.rejects(new Error("unexpected gcf.setIamPolicy")); - gcf.setInvokerCreate.rejects(new Error("unexpected gcf.setInvokerCreate")); - gcf.setInvokerUpdate.rejects(new Error("unexpected gcf.setInvokerUpdate")); - gcfv2.createFunction.rejects(new Error("unexpected gcfv2.createFunction")); - gcfv2.updateFunction.rejects(new Error("unexpected gcfv2.updateFunction")); - gcfv2.deleteFunction.rejects(new Error("unexpected gcfv2.deleteFunction")); - run.getIamPolicy.rejects(new Error("unexpected run.getIamPolicy")); - run.setIamPolicy.rejects(new Error("unexpected run.setIamPolicy")); - run.setInvokerCreate.rejects(new Error("unexpected run.setInvokerCreate")); - run.setInvokerUpdate.rejects(new Error("unexpected run.setInvokerUpdate")); - run.replaceService.rejects(new Error("unexpected run.replaceService")); - poller.pollOperation.rejects(new Error("unexpected poller.pollOperation")); - pubsub.createTopic.rejects(new Error("unexpected pubsub.createTopic")); - pubsub.deleteTopic.rejects(new Error("unexpected pubsub.deleteTopic")); - scheduler.createOrReplaceJob.rejects(new Error("unexpected scheduler.createOrReplaceJob")); - scheduler.deleteJob.rejects(new Error("unexpected scheduler.deleteJob")); - tasks.upsertQueue.rejects(new Error("unexpected tasks.upsertQueue")); - tasks.createQueue.rejects(new Error("unexpected tasks.createQueue")); - tasks.updateQueue.rejects(new Error("unexpected tasks.updateQueue")); - tasks.deleteQueue.rejects(new Error("unexpected tasks.deleteQueue")); - tasks.setEnqueuer.rejects(new Error("unexpected tasks.setEnqueuer")); - tasks.setIamPolicy.rejects(new Error("unexpected tasks.setIamPolicy")); - tasks.getIamPolicy.rejects(new Error("unexpected tasks.getIamPolicy")); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - const storage: gcfNSV2.StorageSource = { - bucket: "bucket", - object: "object", - generation: 42, - }; - const ctorArgs: fabricator.FabricatorArgs = { - executor: new executor.InlineExecutor(), - functionExecutor: new executor.InlineExecutor(), - sourceUrl: "https://example.com", - storage: { - "us-central1": storage, - "us-west1": storage, - }, - appEngineLocation: "us-central1", - }; - let fab: fabricator.Fabricator; - beforeEach(() => { - fab = new fabricator.Fabricator(ctorArgs); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - function endpoint( - trigger: backend.Triggered = { httpsTrigger: {} }, - base: Partial = {} - ): backend.Endpoint { - return { - platform: "gcfv1", - id: "id", - region: "us-central1", - entryPoint: "entrypoint", - runtime: "nodejs16", - ...JSON.parse(JSON.stringify(base)), - ...trigger, - } as backend.Endpoint; - } - - describe("createV1Function", () => { - it("throws on create function failure", async () => { - gcf.createFunction.rejects(new Error("Server failure")); - - await expect( - fab.createV1Function(endpoint(), new scraper.SourceTokenScraper()) - ).to.be.rejectedWith(reporter.DeploymentError, "create"); - - gcf.createFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.rejects(new Error("Fail whale")); - await expect( - fab.createV1Function(endpoint(), new scraper.SourceTokenScraper()) - ).to.be.rejectedWith(reporter.DeploymentError, "create"); - }); - - it("throws on set invoker failure", async () => { - gcf.createFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerCreate.rejects(new Error("Boom")); - - await expect( - fab.createV1Function(endpoint(), new scraper.SourceTokenScraper()) - ).to.be.rejectedWith(reporter.DeploymentError, "set invoker"); - }); - - it("enforces SECURE_ALWAYS HTTPS policies", async () => { - gcf.createFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerCreate.resolves(); - const ep = endpoint(); - - await fab.createV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.createFunction).to.have.been.calledWithMatch({ - httpsTrigger: { - securityLevel: "SECURE_ALWAYS", - }, - }); - }); - - it("sets invoker by default", async () => { - gcf.createFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerCreate.resolves(); - const ep = endpoint(); - - await fab.createV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.setInvokerCreate).to.have.been.calledWith(ep.project, backend.functionName(ep), [ - "public", - ]); - }); - - it("sets explicit invoker", async () => { - gcf.createFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerCreate.resolves(); - const ep = endpoint({ - httpsTrigger: { - invoker: ["custom@"], - }, - }); - - await fab.createV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.setInvokerCreate).to.have.been.calledWith(ep.project, backend.functionName(ep), [ - "custom@", - ]); - }); - - it("doesn't set private invoker on create", async () => { - gcf.createFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerCreate.resolves(); - const ep = endpoint({ - httpsTrigger: { - invoker: ["private"], - }, - }); - - await fab.createV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.setInvokerCreate).to.not.have.been.called; - }); - - it("doesn't set invoker on non-http functions", async () => { - gcf.createFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerCreate.resolves(); - const ep = endpoint({ - scheduleTrigger: {}, - }); - - await fab.createV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.setInvokerCreate).to.not.have.been.called; - }); - }); - - describe("updateV1Function", () => { - it("throws on update function failure", async () => { - gcf.updateFunction.rejects(new Error("Server failure")); - - await expect( - fab.updateV1Function(endpoint(), new scraper.SourceTokenScraper()) - ).to.be.rejectedWith(reporter.DeploymentError, "update"); - - gcf.updateFunction.resolves({ name: "op", type: "update", done: false }); - poller.pollOperation.rejects(new Error("Fail whale")); - await expect( - fab.updateV1Function(endpoint(), new scraper.SourceTokenScraper()) - ).to.be.rejectedWith(reporter.DeploymentError, "update"); - }); - - it("throws on set invoker failure", async () => { - gcf.updateFunction.resolves({ name: "op", type: "update", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerUpdate.rejects(new Error("Boom")); - - const ep = endpoint({ - httpsTrigger: { - invoker: ["private"], - }, - }); - await expect(fab.updateV1Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( - reporter.DeploymentError, - "set invoker" - ); - }); - - it("sets explicit invoker", async () => { - gcf.updateFunction.resolves({ name: "op", type: "create", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerUpdate.resolves(); - const ep = endpoint({ - httpsTrigger: { - invoker: ["custom@"], - }, - }); - - await fab.updateV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.setInvokerUpdate).to.have.been.calledWith(ep.project, backend.functionName(ep), [ - "custom@", - ]); - }); - - it("does not set invoker by default", async () => { - gcf.updateFunction.resolves({ name: "op", type: "update", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerUpdate.resolves(); - const ep = endpoint(); - - await fab.updateV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.setInvokerUpdate).to.not.have.been.called; - }); - - it("doesn't set invoker on non-http functions", async () => { - gcf.updateFunction.resolves({ name: "op", type: "update", done: false }); - poller.pollOperation.resolves(); - gcf.setInvokerUpdate.resolves(); - const ep = endpoint({ - scheduleTrigger: {}, - }); - - await fab.updateV1Function(ep, new scraper.SourceTokenScraper()); - expect(gcf.setInvokerUpdate).to.not.have.been.called; - }); - }); - - describe("deleteV1Function", () => { - it("throws on delete function failure", async () => { - gcf.deleteFunction.rejects(new Error("404")); - const ep = endpoint(); - - await expect(fab.deleteV1Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "delete"); - - gcf.deleteFunction.resolves({ name: "op", type: "delete", done: false }); - poller.pollOperation.rejects(new Error("5xx")); - - await expect(fab.deleteV1Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "delete"); - }); - }); - - describe("createV2Function", () => { - let setConcurrency: sinon.SinonStub; - - beforeEach(() => { - setConcurrency = sinon.stub(fab, "setConcurrency"); - setConcurrency.resolves(); - }); - - it("handles topiocs that already exist", async () => { - pubsub.createTopic.callsFake(() => { - const err = new Error("Already exists"); - (err as any).status = 409; - return Promise.reject(err); - }); - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - - const ep = endpoint( - { - eventTrigger: { - eventType: gcfv2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "topic", - }, - retry: false, - }, - }, - { - platform: "gcfv2", - } - ); - - await fab.createV2Function(ep); - expect(pubsub.createTopic).to.have.been.called; - expect(gcfv2.createFunction).to.have.been.called; - }); - - it("handles failures to create a topic", async () => { - pubsub.createTopic.rejects(new Error("🤷‍♂️")); - - const ep = endpoint( - { - eventTrigger: { - eventType: gcfv2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "topic", - }, - retry: false, - }, - }, - { - platform: "gcfv2", - } - ); - - await expect(fab.createV2Function(ep)).to.be.rejectedWith( - reporter.DeploymentError, - "create topic" - ); - }); - - it("throws on create function failure", async () => { - gcfv2.createFunction.rejects(new Error("Server failure")); - - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await expect(fab.createV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "create"); - - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.rejects(new Error("Fail whale")); - - await expect(fab.createV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "create"); - }); - - it("throws on set invoker failure", async () => { - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerCreate.rejects(new Error("Boom")); - - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await expect(fab.createV2Function(ep)).to.be.rejectedWith( - reporter.DeploymentError, - "set invoker" - ); - }); - - it("sets invoker and concurrency by default", async () => { - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerCreate.resolves(); - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - - await fab.createV2Function(ep); - expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["public"]); - expect(setConcurrency).to.have.been.calledWith(ep, "service", 80); - }); - - it("sets explicit invoker", async () => { - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerCreate.resolves(); - const ep = endpoint( - { - httpsTrigger: { - invoker: ["custom@"], - }, - }, - { platform: "gcfv2" } - ); - - await fab.createV2Function(ep); - expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["custom@"]); - }); - - it("doesn't set private invoker on create", async () => { - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerCreate.resolves(); - const ep = endpoint({ httpsTrigger: { invoker: ["private"] } }, { platform: "gcfv2" }); - - await fab.createV2Function(ep); - expect(gcf.setInvokerCreate).to.not.have.been.called; - }); - - it("doesn't set invoker on non-http functions", async () => { - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerCreate.resolves(); - const ep = endpoint({ scheduleTrigger: {} }, { platform: "gcfv2" }); - - await fab.createV2Function(ep); - expect(run.setInvokerCreate).to.not.have.been.called; - }); - - it("sets explicit concurrency", async () => { - gcfv2.createFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerCreate.resolves(); - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2", concurrency: 1 }); - - await fab.createV2Function(ep); - expect(setConcurrency).to.have.been.calledWith(ep, "service", 1); - }); - }); - - describe("updateV2Function", () => { - it("throws on update function failure", async () => { - gcfv2.updateFunction.rejects(new Error("Server failure")); - - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await expect(fab.updateV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "update"); - - gcfv2.updateFunction.resolves({ name: "op", done: false }); - poller.pollOperation.rejects(new Error("Fail whale")); - await expect(fab.updateV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "update"); - }); - - it("throws on set invoker failure", async () => { - gcfv2.updateFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerUpdate.rejects(new Error("Boom")); - - const ep = endpoint({ httpsTrigger: { invoker: ["private"] } }, { platform: "gcfv2" }); - await expect(fab.updateV2Function(ep)).to.be.rejectedWith( - reporter.DeploymentError, - "set invoker" - ); - }); - - it("sets explicit invoker", async () => { - gcfv2.updateFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerUpdate.resolves(); - const ep = endpoint( - { - httpsTrigger: { - invoker: ["custom@"], - }, - }, - { platform: "gcfv2" } - ); - - await fab.updateV2Function(ep); - expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["custom@"]); - }); - - it("does not set invoker by default", async () => { - gcfv2.updateFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerUpdate.resolves(); - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - - await fab.updateV2Function(ep); - expect(run.setInvokerUpdate).to.not.have.been.called; - }); - - it("doesn't set invoker on non-http functions", async () => { - gcfv2.updateFunction.resolves({ name: "op", done: false }); - poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); - run.setInvokerUpdate.resolves(); - const ep = endpoint({ scheduleTrigger: {} }, { platform: "gcfv2" }); - - await fab.updateV2Function(ep); - expect(run.setInvokerUpdate).to.not.have.been.called; - }); - }); - - describe("deleteV2Function", () => { - it("throws on delete function failure", async () => { - gcfv2.deleteFunction.rejects(new Error("404")); - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - - await expect(fab.deleteV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "delete"); - - gcfv2.deleteFunction.resolves({ name: "op", done: false }); - poller.pollOperation.rejects(new Error("5xx")); - - await expect(fab.deleteV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "delete"); - }); - }); - - describe("setConcurrency", () => { - let service: runNS.Service; - beforeEach(() => { - service = { - apiVersion: "serving.knative.dev/v1", - kind: "service", - metadata: { - name: "service", - namespace: "project", - }, - spec: { - template: { - metadata: { - name: "service", - namespace: "project", - }, - spec: { - containerConcurrency: 80, - }, - }, - traffic: [], - }, - }; - }); - - it("sets concurrency when necessary", async () => { - run.getService.resolves(service); - run.replaceService.callsFake((name: string, svc: runNS.Service) => { - expect(svc.spec.template.spec.containerConcurrency).equals(1); - // Run throws if this field is set - expect(svc.spec.template.metadata.name).is.undefined; - return Promise.resolve(service); - }); - - await fab.setConcurrency(endpoint(), "service", 1); - expect(run.replaceService).to.have.been.called; - }); - - it("doesn't set concurrency when already at the correct value", async () => { - run.getService.resolves(service); - - await fab.setConcurrency( - endpoint(), - "service", - service.spec.template.spec.containerConcurrency! - ); - expect(run.replaceService).to.not.have.been.called; - }); - - it("wraps errors", async () => { - run.getService.rejects(new Error("Oh noes!")); - - await expect(fab.setConcurrency(endpoint(), "service", 1)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "set concurrency" - ); - - run.getService.resolves(service); - run.replaceService.rejects(new Error("read only")); - await expect(fab.setConcurrency(endpoint(), "service", 1)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "set concurrency" - ); - }); - }); - - describe("upsertScheduleV1", () => { - const ep = endpoint({ - scheduleTrigger: { - schedule: "every 5 minutes", - }, - }) as backend.Endpoint & backend.ScheduleTriggered; - - it("upserts schedules", async () => { - scheduler.createOrReplaceJob.resolves(); - await fab.upsertScheduleV1(ep); - expect(scheduler.createOrReplaceJob).to.have.been.called; - }); - - it("wraps errors", async () => { - scheduler.createOrReplaceJob.rejects(new Error("Fail")); - await expect(fab.upsertScheduleV1(ep)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "upsert schedule" - ); - }); - }); - - describe("deleteScheduleV1", () => { - const ep = endpoint({ - scheduleTrigger: { - schedule: "every 5 minutes", - }, - }) as backend.Endpoint & backend.ScheduleTriggered; - - it("deletes schedules and topics", async () => { - scheduler.deleteJob.resolves(); - pubsub.deleteTopic.resolves(); - await fab.deleteScheduleV1(ep); - expect(scheduler.deleteJob).to.have.been.called; - expect(pubsub.deleteTopic).to.have.been.called; - }); - - it("wraps errors", async () => { - scheduler.deleteJob.rejects(new Error("Fail")); - await expect(fab.deleteScheduleV1(ep)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "delete schedule" - ); - - scheduler.deleteJob.resolves(); - pubsub.deleteTopic.rejects(new Error("Fail")); - await expect(fab.deleteScheduleV1(ep)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "delete topic" - ); - }); - }); - - describe("upsertTaskQueue", () => { - it("upserts task queues", async () => { - const ep = endpoint({ - taskQueueTrigger: {}, - }) as backend.Endpoint & backend.TaskQueueTriggered; - tasks.upsertQueue.resolves(); - await fab.upsertTaskQueue(ep); - expect(tasks.upsertQueue).to.have.been.called; - expect(tasks.setEnqueuer).to.not.have.been.called; - }); - - it("sets enqueuer", async () => { - const ep = endpoint({ - taskQueueTrigger: { - invoker: ["public"], - }, - }) as backend.Endpoint & backend.TaskQueueTriggered; - tasks.upsertQueue.resolves(); - tasks.setEnqueuer.resolves(); - await fab.upsertTaskQueue(ep); - expect(tasks.upsertQueue).to.have.been.called; - expect(tasks.setEnqueuer).to.have.been.calledWithMatch(tasks.queueNameForEndpoint(ep), [ - "public", - ]); - }); - - it("wraps errors", async () => { - const ep = endpoint({ - taskQueueTrigger: { - invoker: ["public"], - }, - }) as backend.Endpoint & backend.TaskQueueTriggered; - tasks.upsertQueue.rejects(new Error("oh no")); - await expect(fab.upsertTaskQueue(ep)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "upsert task queue" - ); - - tasks.upsertQueue.resolves(); - tasks.setEnqueuer.rejects(new Error("nope")); - await expect(fab.upsertTaskQueue(ep)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "set invoker" - ); - }); - }); - - describe("disableTaskQueue", () => { - it("disables task queues", async () => { - const ep = endpoint({ - taskQueueTrigger: {}, - }) as backend.Endpoint & backend.TaskQueueTriggered; - tasks.updateQueue.resolves(); - await fab.disableTaskQueue(ep); - expect(tasks.updateQueue).to.have.been.calledWith({ - name: tasks.queueNameForEndpoint(ep), - state: "DISABLED", - }); - }); - - it("wraps errors", async () => { - const ep = endpoint({ - taskQueueTrigger: {}, - }) as backend.Endpoint & backend.TaskQueueTriggered; - tasks.updateQueue.rejects(new Error("Not today")); - await expect(fab.disableTaskQueue(ep)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "disable task queue" - ); - }); - }); - - describe("setTrigger", () => { - it("does nothing for HTTPS functions", async () => { - // all APIs throw by default - await fab.setTrigger(endpoint({ httpsTrigger: {} })); - }); - - it("does nothing for event triggers", async () => { - // all APIs throw by default - const ep = endpoint({ - eventTrigger: { - eventType: gcfNSV2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "topic", - }, - retry: false, - }, - }); - await fab.setTrigger(ep); - }); - - it("sets schedule triggers", async () => { - const ep = endpoint({ - scheduleTrigger: { - schedule: "every 5 minutes", - }, - }); - const upsertScheduleV1 = sinon.stub(fab, "upsertScheduleV1"); - upsertScheduleV1.resolves(); - - await fab.setTrigger(ep); - expect(upsertScheduleV1).to.have.been.called; - upsertScheduleV1.restore(); - - ep.platform = "gcfv2"; - const upsertScheduleV2 = sinon.stub(fab, "upsertScheduleV2"); - upsertScheduleV2.resolves(); - - await fab.setTrigger(ep); - expect(upsertScheduleV2).to.have.been.called; - }); - - it("sets task queue triggers", async () => { - const ep = endpoint({ - taskQueueTrigger: {}, - }); - const upsertTaskQueue = sinon.stub(fab, "upsertTaskQueue"); - upsertTaskQueue.resolves(); - - await fab.setTrigger(ep); - expect(upsertTaskQueue).to.have.been.called; - }); - }); - - describe("deleteTrigger", () => { - it("does nothing for HTTPS functions", async () => { - // all APIs throw by default - await fab.deleteTrigger(endpoint({ httpsTrigger: {} })); - }); - - it("does nothing for event triggers", async () => { - // all APIs throw by default - const ep = endpoint({ - eventTrigger: { - eventType: gcfNSV2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "topic", - }, - retry: false, - }, - }); - await fab.deleteTrigger(ep); - }); - - it("deletes schedule triggers", async () => { - const ep = endpoint({ - scheduleTrigger: { - schedule: "every 5 minutes", - }, - }); - const deleteScheduleV1 = sinon.stub(fab, "deleteScheduleV1"); - deleteScheduleV1.resolves(); - - await fab.deleteTrigger(ep); - expect(deleteScheduleV1).to.have.been.called; - deleteScheduleV1.restore(); - - ep.platform = "gcfv2"; - const deleteScheduleV2 = sinon.stub(fab, "deleteScheduleV2"); - deleteScheduleV2.resolves(); - - await fab.deleteTrigger(ep); - expect(deleteScheduleV2).to.have.been.called; - }); - - it("deletes task queue triggers", async () => { - const ep = endpoint({ - taskQueueTrigger: {}, - }); - const disableTaskQueue = sinon.stub(fab, "disableTaskQueue"); - - await fab.deleteTrigger(ep); - expect(disableTaskQueue).to.have.been.called; - }); - }); - - describe("createEndpoint", () => { - it("creates v1 functions", async () => { - const ep = endpoint(); - const setTrigger = sinon.stub(fab, "setTrigger"); - setTrigger.resolves(); - const createV1Function = sinon.stub(fab, "createV1Function"); - createV1Function.resolves(); - - await fab.createEndpoint(ep, new scraper.SourceTokenScraper()); - expect(createV1Function).is.calledOnce; - expect(setTrigger).is.calledOnce; - expect(setTrigger).is.calledAfter(createV1Function); - }); - - it("creates v2 functions", async () => { - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - const setTrigger = sinon.stub(fab, "setTrigger"); - setTrigger.resolves(); - const createV2Function = sinon.stub(fab, "createV2Function"); - createV2Function.resolves(); - - await fab.createEndpoint(ep, new scraper.SourceTokenScraper()); - expect(createV2Function).is.calledOnce; - expect(setTrigger).is.calledOnce; - expect(setTrigger).is.calledAfter(createV2Function); - }); - - it("aborts for failures midway", async () => { - const ep = endpoint(); - const setTrigger = sinon.stub(fab, "setTrigger"); - const createV1Function = sinon.stub(fab, "createV1Function"); - createV1Function.rejects(new reporter.DeploymentError(ep, "set invoker", undefined)); - - await expect(fab.createEndpoint(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( - reporter.DeploymentError, - "set invoker" - ); - expect(createV1Function).is.calledOnce; - expect(setTrigger).is.not.called; - }); - }); - - describe("updateEndpoint", () => { - it("updates v1 functions", async () => { - const ep = endpoint(); - const setTrigger = sinon.stub(fab, "setTrigger"); - setTrigger.resolves(); - const updateV1Function = sinon.stub(fab, "updateV1Function"); - updateV1Function.resolves(); - - await fab.updateEndpoint({ endpoint: ep }, new scraper.SourceTokenScraper()); - expect(updateV1Function).is.calledOnce; - expect(setTrigger).is.calledOnce; - expect(setTrigger).is.calledAfter(updateV1Function); - }); - - it("updates v2 functions", async () => { - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - const setTrigger = sinon.stub(fab, "setTrigger"); - setTrigger.resolves(); - const updateV2Function = sinon.stub(fab, "updateV2Function"); - updateV2Function.resolves(); - - await fab.updateEndpoint({ endpoint: ep }, new scraper.SourceTokenScraper()); - expect(updateV2Function).is.calledOnce; - expect(setTrigger).is.calledOnce; - expect(setTrigger).is.calledAfter(updateV2Function); - }); - - it("aborts for failures midway", async () => { - const ep = endpoint(); - const setTrigger = sinon.stub(fab, "setTrigger"); - const updateV1Function = sinon.stub(fab, "updateV1Function"); - updateV1Function.rejects(new reporter.DeploymentError(ep, "set invoker", undefined)); - - await expect( - fab.updateEndpoint({ endpoint: ep }, new scraper.SourceTokenScraper()) - ).to.be.rejectedWith(reporter.DeploymentError, "set invoker"); - expect(updateV1Function).is.calledOnce; - expect(setTrigger).is.not.called; - }); - - it("can delete and create", async () => { - const target = endpoint( - { scheduleTrigger: { schedule: "every 5 minutes" } }, - { platform: "gcfv2" } - ); - const before = endpoint( - { scheduleTrigger: { schedule: "every 5 minutes" } }, - { platform: "gcfv1" } - ); - const update = { - endpoint: target, - deleteAndRecreate: before, - }; - - const deleteTrigger = sinon.stub(fab, "deleteTrigger"); - deleteTrigger.resolves(); - const setTrigger = sinon.stub(fab, "setTrigger"); - setTrigger.resolves(); - const deleteV1Function = sinon.stub(fab, "deleteV1Function"); - deleteV1Function.resolves(); - const createV2Function = sinon.stub(fab, "createV2Function"); - createV2Function.resolves(); - - await fab.updateEndpoint(update, new scraper.SourceTokenScraper()); - - expect(deleteTrigger).to.have.been.called; - expect(deleteV1Function).to.have.been.calledImmediatelyAfter(deleteTrigger); - expect(createV2Function).to.have.been.calledImmediatelyAfter(deleteV1Function); - expect(setTrigger).to.have.been.calledImmediatelyAfter(createV2Function); - }); - }); - - describe("deleteEndpoint", () => { - it("deletes v1 functions", async () => { - const ep = endpoint(); - const deleteTrigger = sinon.stub(fab, "deleteTrigger"); - deleteTrigger.resolves(); - const deleteV1Function = sinon.stub(fab, "deleteV1Function"); - deleteV1Function.resolves(); - - await fab.deleteEndpoint(ep); - expect(deleteTrigger).to.have.been.called; - expect(deleteV1Function).to.have.been.calledImmediatelyAfter(deleteTrigger); - }); - - it("deletes v2 functions", async () => { - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - const deleteTrigger = sinon.stub(fab, "deleteTrigger"); - deleteTrigger.resolves(); - const deleteV2Function = sinon.stub(fab, "deleteV2Function"); - deleteV2Function.resolves(); - - await fab.deleteEndpoint(ep); - expect(deleteTrigger).to.have.been.called; - expect(deleteV2Function).to.have.been.calledImmediatelyAfter(deleteTrigger); - }); - - it("does not delete functions with triggers outstanding", async () => { - const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - const deleteV2Function = sinon.stub(fab, "deleteV2Function"); - const deleteTrigger = sinon.stub(fab, "deleteTrigger"); - deleteTrigger.rejects(new reporter.DeploymentError(ep, "delete schedule", undefined)); - deleteV2Function.resolves(); - - await expect(fab.deleteEndpoint(ep)).to.eventually.be.rejected; - expect(deleteV2Function).to.not.have.been.called; - }); - }); - - describe("applyRegionalUpdates", () => { - it("shares source token scrapers across upserts", async () => { - const ep1 = endpoint({ httpsTrigger: {} }, { id: "A" }); - const ep2 = endpoint({ httpsTrigger: {} }, { id: "B" }); - const ep3 = endpoint({ httpsTrigger: {} }, { id: "C" }); - const changes: planner.RegionalChanges = { - endpointsToCreate: [ep1, ep2], - endpointsToUpdate: [{ endpoint: ep3 }], - endpointsToDelete: [], - }; - - let sourceTokenScraper: scraper.SourceTokenScraper | undefined; - let callCount = 0; - const fakeUpsert = ( - unused: backend.Endpoint | planner.EndpointUpdate, - s: scraper.SourceTokenScraper - ): Promise => { - callCount++; - if (!sourceTokenScraper) { - expect(callCount).to.equal(1); - sourceTokenScraper = s; - } - expect(s).to.equal(sourceTokenScraper); - return Promise.resolve(); - }; - - const createEndpoint = sinon.stub(fab, "createEndpoint"); - createEndpoint.callsFake(fakeUpsert); - const updateEndpoint = sinon.stub(fab, "updateEndpoint"); - updateEndpoint.callsFake(fakeUpsert); - - await fab.applyRegionalChanges(changes); - }); - - it("handles errors and wraps them in results", async () => { - // when it hits a real API it will fail. - const ep = endpoint(); - const changes: planner.RegionalChanges = { - endpointsToCreate: [ep], - endpointsToUpdate: [], - endpointsToDelete: [], - }; - - const results = await fab.applyRegionalChanges(changes); - expect(results[0].error).to.be.instanceOf(reporter.DeploymentError); - expect(results[0].error?.message).to.match(/create function/); - }); - }); - - it("does not delete if there are upsert errors", async () => { - // when it hits a real API it will fail. - const createEP = endpoint({ httpsTrigger: {} }, { id: "A" }); - const deleteEP = endpoint({ httpsTrigger: {} }, { id: "B" }); - const changes: planner.RegionalChanges = { - endpointsToCreate: [createEP], - endpointsToUpdate: [], - endpointsToDelete: [deleteEP], - }; - - const results = await fab.applyRegionalChanges(changes); - const result = results.find((r) => r.endpoint.id === deleteEP.id); - expect(result?.error).to.be.instanceOf(reporter.AbortedDeploymentError); - expect(result?.durationMs).to.equal(0); - }); - - it("applies all kinds of changes", async () => { - const createEP = endpoint({ httpsTrigger: {} }, { id: "A" }); - const updateEP = endpoint({ httpsTrigger: {} }, { id: "B" }); - const deleteEP = endpoint({ httpsTrigger: {} }, { id: "C" }); - const update: planner.EndpointUpdate = { endpoint: updateEP }; - const changes: planner.RegionalChanges = { - endpointsToCreate: [createEP], - endpointsToUpdate: [update], - endpointsToDelete: [deleteEP], - }; - - const createEndpoint = sinon.stub(fab, "createEndpoint"); - createEndpoint.resolves(); - const updateEndpoint = sinon.stub(fab, "updateEndpoint"); - updateEndpoint.resolves(); - const deleteEndpoint = sinon.stub(fab, "deleteEndpoint"); - deleteEndpoint.resolves(); - - const results = await fab.applyRegionalChanges(changes); - expect(createEndpoint).to.have.been.calledWithMatch(createEP); - expect(updateEndpoint).to.have.been.calledWithMatch(update); - expect(deleteEndpoint).to.have.been.calledWith(deleteEP); - - // We can't actually verify that the timing isn't zero because tests - // have run in <1ms and failed. - expect(results[0].error).to.be.undefined; - expect(results[1].error).to.be.undefined; - expect(results[2].error).to.be.undefined; - }); - - describe("applyPlan", () => { - it("fans out to regions", async () => { - const ep1 = endpoint({ httpsTrigger: {} }, { region: "us-central1" }); - const ep2 = endpoint({ httpsTrigger: {} }, { region: "us-west1" }); - const plan: planner.DeploymentPlan = { - "us-central1": { - endpointsToCreate: [ep1], - endpointsToUpdate: [], - endpointsToDelete: [], - }, - "us-west1": { - endpointsToCreate: [], - endpointsToUpdate: [], - endpointsToDelete: [ep2], - }, - }; - - // Will fail when it hits actual API calls - const summary = await fab.applyPlan(plan); - const ep1Result = summary.results.find((r) => r.endpoint.region == ep1.region); - expect(ep1Result?.error).to.be.instanceOf(reporter.DeploymentError); - expect(ep1Result?.error?.message).to.match(/create function/); - - const ep2Result = summary.results.find((r) => r.endpoint.region === ep2.region); - expect(ep2Result?.error).to.be.instanceOf(reporter.DeploymentError); - expect(ep2Result?.error?.message).to.match(/delete function/); - }); - }); -}); diff --git a/src/test/deploy/functions/release/planner.spec.ts b/src/test/deploy/functions/release/planner.spec.ts deleted file mode 100644 index d5dd56b6bbc..00000000000 --- a/src/test/deploy/functions/release/planner.spec.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as backend from "../../../../deploy/functions/backend"; -import * as planner from "../../../../deploy/functions/release/planner"; -import * as deploymentTool from "../../../../deploymentTool"; -import * as gcfv2 from "../../../../gcp/cloudfunctionsv2"; -import * as utils from "../../../../utils"; - -describe("planner", () => { - let logLabeledBullet: sinon.SinonStub; - - function allowV2Upgrades(): void { - sinon.stub(planner, "checkForV2Upgrade"); - } - - beforeEach(() => { - logLabeledBullet = sinon.stub(utils, "logLabeledBullet"); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - function func( - id: string, - region: string, - triggered: backend.Triggered = { httpsTrigger: {} } - ): backend.Endpoint { - return { - id, - region, - ...triggered, - platform: "gcfv1", - project: "project", - runtime: "nodejs16", - entryPoint: "function", - environmentVariables: {}, - } as backend.Endpoint; - } - - describe("calculateUpdate", () => { - it("throws on illegal updates", () => { - const httpsFunc = func("a", "b", { httpsTrigger: {} }); - const scheduleFunc = func("a", "b", { scheduleTrigger: {} }); - expect(() => planner.calculateUpdate(httpsFunc, scheduleFunc)).to.throw; - }); - - it("knows to delete & recreate for v2 topic changes", () => { - const original: backend.Endpoint = { - ...func("a", "b", { - eventTrigger: { - eventType: gcfv2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "topic", - }, - retry: false, - }, - }), - platform: "gcfv2", - }; - const changed = JSON.parse(JSON.stringify(original)) as backend.Endpoint; - if (backend.isEventTriggered(changed)) { - changed.eventTrigger.eventFilters["resource"] = "anotherTopic"; - } - expect(planner.calculateUpdate(changed, original)).to.deep.equal({ - endpoint: changed, - deleteAndRecreate: original, - }); - }); - - it("knows to delete & recreate for v1 to v2 scheduled function upgrades", () => { - const original: backend.Endpoint = { - ...func("a", "b", { scheduleTrigger: {} }), - platform: "gcfv1", - }; - const changed: backend.Endpoint = { ...original, platform: "gcfv2" }; - - allowV2Upgrades(); - expect(planner.calculateUpdate(changed, original)).to.deep.equal({ - endpoint: changed, - deleteAndRecreate: original, - }); - }); - - it("knows to delete & recreate when trigger regions change", () => { - const original: backend.Endpoint = func("a", "b", { - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalzied", - eventFilters: { - bucket: "mybucket", - }, - region: "us-west1", - retry: false, - }, - }); - original.platform = "gcfv2"; - const changed: backend.Endpoint = func("a", "b", { - eventTrigger: { - eventType: "google.cloud.storage.object.v1.finalzied", - eventFilters: { - bucket: "bucket2", - }, - region: "us", - retry: false, - }, - }); - changed.platform = "gcfv2"; - allowV2Upgrades(); - expect(planner.calculateUpdate(changed, original)).to.deep.equal({ - endpoint: changed, - deleteAndRecreate: original, - }); - }); - - it("knows to upgrade in-place in the general case", () => { - const v1Function: backend.Endpoint = { - ...func("a", "b"), - platform: "gcfv1", - }; - const v2Function: backend.Endpoint = { - ...v1Function, - platform: "gcfv1", - availableMemoryMb: 512, - }; - expect(planner.calculateUpdate(v2Function, v1Function)).to.deep.equal({ - endpoint: v2Function, - }); - }); - }); - - describe("calculateRegionalChanges", () => { - it("passes a smoke test", () => { - const created = func("created", "region"); - const updated = func("updated", "region"); - const deleted = func("deleted", "region"); - deleted.labels = deploymentTool.labels(); - const pantheon = func("pantheon", "region"); - - const want = { created, updated }; - const have = { updated, deleted, pantheon }; - - // note: pantheon is not updated in any way - expect(planner.calculateRegionalChanges(want, have, {})).to.deep.equal({ - endpointsToCreate: [created], - endpointsToUpdate: [ - { - endpoint: updated, - }, - ], - endpointsToDelete: [deleted], - }); - }); - - it("can be told to delete all functions", () => { - const created = func("created", "region"); - const updated = func("updated", "region"); - const deleted = func("deleted", "region"); - deleted.labels = deploymentTool.labels(); - const pantheon = func("pantheon", "region"); - - const want = { created, updated }; - const have = { updated, deleted, pantheon }; - - // note: pantheon is deleted because we have deleteAll: true - expect(planner.calculateRegionalChanges(want, have, { deleteAll: true })).to.deep.equal({ - endpointsToCreate: [created], - endpointsToUpdate: [ - { - endpoint: updated, - }, - ], - endpointsToDelete: [deleted, pantheon], - }); - }); - }); - - describe("createDeploymentPlan", () => { - it("applies filters", () => { - const group1Created = func("g1-created", "region"); - const group1Updated = func("g1-updated", "region"); - const group1Deleted = func("g1-deleted", "region"); - - const group2Created = func("g2-created", "region"); - const group2Updated = func("g2-updated", "region"); - const group2Deleted = func("g2-deleted", "region"); - - group1Deleted.labels = deploymentTool.labels(); - group2Deleted.labels = deploymentTool.labels(); - - const want = backend.of(group1Updated, group1Created, group2Updated, group2Created); - const have = backend.of(group1Updated, group1Deleted, group2Updated, group2Deleted); - - expect(planner.createDeploymentPlan(want, have, { filters: [["g1"]] })).to.deep.equal({ - region: { - endpointsToCreate: [group1Created], - endpointsToUpdate: [ - { - endpoint: group1Updated, - }, - ], - endpointsToDelete: [group1Deleted], - }, - }); - }); - - it("nudges users towards concurrency settings when upgrading and not setting", () => { - const original: backend.Endpoint = func("id", "region"); - original.platform = "gcfv1"; - const upgraded: backend.Endpoint = { ...original }; - upgraded.platform = "gcfv2"; - - const have = backend.of(original); - const want = backend.of(upgraded); - - allowV2Upgrades(); - planner.createDeploymentPlan(want, have); - expect(logLabeledBullet).to.have.been.calledOnceWith( - "functions", - sinon.match(/change this with the 'concurrency' option/) - ); - }); - }); - - it("does not warn users about concurrency when inappropriate", () => { - allowV2Upgrades(); - // Concurrency isn't set but this isn't an upgrade operation, so there - // should be no warning - const v2Function: backend.Endpoint = { ...func("id", "region"), platform: "gcfv2" }; - - planner.createDeploymentPlan(backend.of(v2Function), backend.of(v2Function)); - expect(logLabeledBullet).to.not.have.been.called; - - const v1Function: backend.Endpoint = { ...func("id", "region"), platform: "gcfv1" }; - planner.createDeploymentPlan(backend.of(v1Function), backend.of(v1Function)); - expect(logLabeledBullet).to.not.have.been.called; - - // Upgraded but specified concurrency - const concurrencyUpgraded: backend.Endpoint = { - ...v1Function, - platform: "gcfv2", - concurrency: 80, - }; - planner.createDeploymentPlan(backend.of(concurrencyUpgraded), backend.of(v1Function)); - expect(logLabeledBullet).to.not.have.been.called; - }); - - describe("checkForIllegalUpdate", () => { - // TODO: delete this test once GCF supports upgrading from v1 to v2 - it("prohibits upgrades from v1 to v2", () => { - const have: backend.Endpoint = { ...func("id", "region"), platform: "gcfv1" }; - const want: backend.Endpoint = { ...func("id", "region"), platform: "gcfv2" }; - - expect(() => planner.checkForIllegalUpdate(want, have)).to.throw; - }); - - it("should throw if a https function would be changed into an event triggered function", () => { - const want = func("a", "b", { - eventTrigger: { - eventType: "google.pubsub.topic.publish", - eventFilters: {}, - retry: false, - }, - }); - const have = func("a", "b", { httpsTrigger: {} }); - - expect(() => planner.checkForIllegalUpdate(want, have)).to.throw(); - }); - - it("should throw if a event triggered function would be changed into an https function", () => { - const want = func("a", "b", { httpsTrigger: {} }); - const have = func("a", "b", { - eventTrigger: { - eventType: "google.pubsub.topic.publish", - eventFilters: {}, - retry: false, - }, - }); - - expect(() => planner.checkForIllegalUpdate(want, have)).to.throw(); - }); - - it("should throw if a scheduled trigger would change into an https function", () => { - const want = func("a", "b"); - const have = func("a", "b", { scheduleTrigger: {} }); - - expect(() => planner.checkForIllegalUpdate(want, have)).to.throw(); - }); - - it("should not throw if a event triggered function keeps the same trigger", () => { - const eventTrigger: backend.EventTrigger = { - eventType: "google.pubsub.topic.publish", - eventFilters: {}, - retry: false, - }; - const want = func("a", "b", { eventTrigger }); - - expect(() => planner.checkForIllegalUpdate(want, want)).not.to.throw(); - }); - - it("should not throw if a https function stays as a https function", () => { - const want = func("a", "b"); - const have = func("a", "b"); - - expect(() => planner.checkForIllegalUpdate(want, have)).not.to.throw(); - }); - - it("should not throw if a scheduled function stays as a scheduled function", () => { - const want = func("a", "b", { scheduleTrigger: {} }); - const have = func("a", "b", { scheduleTrigger: {} }); - - expect(() => planner.checkForIllegalUpdate(want, have)).not.to.throw(); - }); - - it("should throw if a user downgrades from v2 to v1", () => { - const want: backend.Endpoint = { ...func("id", "region"), platform: "gcfv1" }; - const have: backend.Endpoint = { ...func("id", "region"), platform: "gcfv2" }; - - expect(() => planner.checkForIllegalUpdate(want, have)).to.throw(); - }); - }); - - it("detects changes to v2 pubsub topics", () => { - const eventTrigger: backend.EventTrigger = { - eventType: gcfv2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "projects/p/topics/t", - }, - retry: false, - }; - - let want: backend.Endpoint = { ...func("id", "region"), platform: "gcfv1" }; - let have: backend.Endpoint = { ...func("id", "region"), platform: "gcfv1" }; - expect(planner.changedV2PubSubTopic(want, have)).to.be.false; - - want.platform = "gcfv2"; - expect(planner.changedV2PubSubTopic(want, have)).to.be.false; - - have.platform = "gcfv2"; - expect(planner.changedV2PubSubTopic(want, have)).to.be.false; - - want = { - ...func("id", "region", { eventTrigger }), - platform: "gcfv2", - }; - expect(planner.changedV2PubSubTopic(want, have)).to.be.false; - - have = { - ...func("id", "region", { eventTrigger }), - platform: "gcfv2", - }; - expect(planner.changedV2PubSubTopic(want, have)).to.be.false; - - // want has a shallow copy of eventTrigger, so we need to duplicate it - // to modify only 'want' - want = JSON.parse(JSON.stringify(want)) as backend.Endpoint; - if (backend.isEventTriggered(want)) { - want.eventTrigger.eventFilters.resource = "projects/p/topics/t2"; - } - expect(planner.changedV2PubSubTopic(want, have)).to.be.true; - }); - - it("detects upgrades to scheduled functions", () => { - const v1Https: backend.Endpoint = { ...func("id", "region"), platform: "gcfv1" }; - const v1Scheduled: backend.Endpoint = { - ...func("id", "region", { scheduleTrigger: {} }), - platform: "gcfv1", - }; - const v2Https: backend.Endpoint = { ...func("id", "region"), platform: "gcfv2" }; - const v2Scheduled: backend.Endpoint = { - ...func("id", "region", { scheduleTrigger: {} }), - platform: "gcfv2", - }; - - expect(planner.upgradedScheduleFromV1ToV2(v1Https, v1Https)).to.be.false; - expect(planner.upgradedScheduleFromV1ToV2(v2Https, v1Https)).to.be.false; - expect(planner.upgradedScheduleFromV1ToV2(v1Scheduled, v1Scheduled)).to.be.false; - expect(planner.upgradedScheduleFromV1ToV2(v2Scheduled, v2Scheduled)).to.be.false; - - // Invalid case but caught elsewhere - expect(planner.upgradedScheduleFromV1ToV2(v2Scheduled, v1Https)).to.be.false; - expect(planner.upgradedScheduleFromV1ToV2(v2Https, v1Scheduled)).to.be.false; - - expect(planner.upgradedScheduleFromV1ToV2(v2Scheduled, v1Scheduled)).to.be.true; - }); -}); diff --git a/src/test/deploy/functions/release/reporter.spec.ts b/src/test/deploy/functions/release/reporter.spec.ts deleted file mode 100644 index d2727022e7d..00000000000 --- a/src/test/deploy/functions/release/reporter.spec.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { logger } from "../../../../logger"; -import * as backend from "../../../../deploy/functions/backend"; -import * as reporter from "../../../../deploy/functions/release/reporter"; -import * as track from "../../../../track"; - -const ENDPOINT_BASE: Omit = { - platform: "gcfv1", - id: "id", - region: "region", - project: "project", - entryPoint: "id", - runtime: "nodejs16", -}; -const ENDPOINT: backend.Endpoint = { ...ENDPOINT_BASE, httpsTrigger: {} }; - -describe("reporter", () => { - describe("triggerTag", () => { - it("detects v1.https", () => { - expect( - reporter.triggerTag({ - ...ENDPOINT_BASE, - httpsTrigger: {}, - }) - ).to.equal("v1.https"); - }); - - it("detects v2.https", () => { - expect( - reporter.triggerTag({ - ...ENDPOINT_BASE, - platform: "gcfv2", - httpsTrigger: {}, - }) - ).to.equal("v2.https"); - }); - - it("detects v1.callable", () => { - expect( - reporter.triggerTag({ - ...ENDPOINT_BASE, - httpsTrigger: {}, - labels: { - "deployment-callable": "true", - }, - }) - ).to.equal("v1.callable"); - }); - - it("detects v2.callable", () => { - expect( - reporter.triggerTag({ - ...ENDPOINT_BASE, - platform: "gcfv2", - httpsTrigger: {}, - labels: { - "deployment-callable": "true", - }, - }) - ).to.equal("v2.callable"); - }); - - it("detects v1.scheduled", () => { - expect( - reporter.triggerTag({ - ...ENDPOINT_BASE, - scheduleTrigger: {}, - }) - ).to.equal("v1.scheduled"); - }); - - it("detects v2.scheduled", () => { - expect( - reporter.triggerTag({ - ...ENDPOINT_BASE, - platform: "gcfv2", - scheduleTrigger: {}, - }) - ).to.equal("v2.scheduled"); - }); - - it("detects others", () => { - expect( - reporter.triggerTag({ - ...ENDPOINT_BASE, - platform: "gcfv2", - eventTrigger: { - eventType: "google.pubsub.topic.publish", - eventFilters: {}, - retry: false, - }, - }) - ).to.equal("google.pubsub.topic.publish"); - }); - }); - - describe("logAndTrackDeployStats", () => { - let trackStub: sinon.SinonStub; - let debugStub: sinon.SinonStub; - - beforeEach(() => { - trackStub = sinon.stub(track, "track"); - debugStub = sinon.stub(logger, "debug"); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - it("tracks global summaries", async () => { - const summary: reporter.Summary = { - totalTime: 2_000, - results: [ - { - endpoint: ENDPOINT, - durationMs: 2_000, - }, - { - endpoint: ENDPOINT, - durationMs: 1_000, - error: new reporter.DeploymentError(ENDPOINT, "update", undefined), - }, - { - endpoint: ENDPOINT, - durationMs: 0, - error: new reporter.AbortedDeploymentError(ENDPOINT), - }, - ], - }; - - await reporter.logAndTrackDeployStats(summary); - - expect(trackStub).to.have.been.calledWith("functions_region_count", "1", 1); - expect(trackStub).to.have.been.calledWith("function_deploy_success", "v1.https", 2_000); - expect(trackStub).to.have.been.calledWith("function_deploy_failure", "v1.https", 1_000); - // Aborts aren't tracked because they would throw off timing metrics - expect(trackStub).to.not.have.been.calledWith("function_deploy_failure", "v1.https", 0); - - expect(debugStub).to.have.been.calledWith("Total Function Deployment time: 2000"); - expect(debugStub).to.have.been.calledWith("3 Functions Deployed"); - expect(debugStub).to.have.been.calledWith("1 Functions Errored"); - expect(debugStub).to.have.been.calledWith("1 Function Deployments Aborted"); - - // The 0ms for an aborted function isn't counted. - expect(debugStub).to.have.been.calledWith("Average Function Deployment time: 1500"); - }); - - it("tracks v1 vs v2 codebases", async () => { - const v1 = { ...ENDPOINT }; - const v2: backend.Endpoint = { ...ENDPOINT, platform: "gcfv2" }; - - const summary: reporter.Summary = { - totalTime: 1_000, - results: [ - { - endpoint: v1, - durationMs: 1_000, - }, - { - endpoint: v2, - durationMs: 1_000, - }, - ], - }; - - await reporter.logAndTrackDeployStats(summary); - expect(trackStub).to.have.been.calledWith("functions_codebase_deploy", "v1+v2", 2); - trackStub.resetHistory(); - - summary.results = [{ endpoint: v1, durationMs: 1_000 }]; - await reporter.logAndTrackDeployStats(summary); - expect(trackStub).to.have.been.calledWith("functions_codebase_deploy", "v1", 1); - trackStub.resetHistory(); - - summary.results = [{ endpoint: v2, durationMs: 1_000 }]; - await reporter.logAndTrackDeployStats(summary); - expect(trackStub).to.have.been.calledWith("functions_codebase_deploy", "v2", 1); - }); - - it("tracks overall success/failure", async () => { - const success: reporter.DeployResult = { - endpoint: ENDPOINT, - durationMs: 1_000, - }; - const failure: reporter.DeployResult = { - endpoint: ENDPOINT, - durationMs: 1_000, - error: new reporter.DeploymentError(ENDPOINT, "create", undefined), - }; - - const summary: reporter.Summary = { - totalTime: 1_000, - results: [success, failure], - }; - - await reporter.logAndTrackDeployStats(summary); - expect(trackStub).to.have.been.calledWith("functions_deploy_result", "partial_success", 1); - expect(trackStub).to.have.been.calledWith("functions_deploy_result", "partial_failure", 1); - expect(trackStub).to.have.been.calledWith( - "functions_deploy_result", - "partial_error_ratio", - 0.5 - ); - trackStub.resetHistory(); - - summary.results = [success]; - await reporter.logAndTrackDeployStats(summary); - expect(trackStub).to.have.been.calledWith("functions_deploy_result", "success", 1); - trackStub.resetHistory(); - - summary.results = [failure]; - await reporter.logAndTrackDeployStats(summary); - expect(trackStub).to.have.been.calledWith("functions_deploy_result", "failure", 1); - }); - }); - - describe("printErrors", () => { - let infoStub: sinon.SinonStub; - - beforeEach(() => { - infoStub = sinon.stub(logger, "info"); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - it("does nothing if there are no errors", () => { - const summary: reporter.Summary = { - totalTime: 1_000, - results: [ - { - endpoint: ENDPOINT, - durationMs: 1_000, - }, - ], - }; - - reporter.printErrors(summary); - - expect(infoStub).to.not.have.been.called; - }); - - it("only prints summaries for non-aborted errors", () => { - const summary: reporter.Summary = { - totalTime: 1_000, - results: [ - { - endpoint: { ...ENDPOINT, id: "failedCreate" }, - durationMs: 1_000, - error: new reporter.DeploymentError(ENDPOINT, "create", undefined), - }, - { - endpoint: { ...ENDPOINT, id: "abortedDelete" }, - durationMs: 0, - error: new reporter.AbortedDeploymentError(ENDPOINT), - }, - ], - }; - - reporter.printErrors(summary); - - // N.B. The lists of functions are printed in one call along with their header - // so that we know why a function label was printed (e.g. abortedDelete shouldn't - // show up in the main list of functions that had deployment errors but should show - // up in the list of functions that weren't deleted). To match these regexes we must - // pass the "s" modifier to regexes to make . capture newlines. - expect(infoStub).to.have.been.calledWithMatch(/Functions deploy had errors.*failedCreate/s); - expect(infoStub).to.not.have.been.calledWithMatch( - /Functions deploy had errors.*abortedDelete/s - ); - }); - - it("prints IAM errors", () => { - const explicit: backend.Endpoint = { - ...ENDPOINT, - httpsTrigger: { - invoker: ["public"], - }, - }; - - const summary: reporter.Summary = { - totalTime: 1_000, - results: [ - { - endpoint: explicit, - durationMs: 1_000, - error: new reporter.DeploymentError(explicit, "set invoker", undefined), - }, - ], - }; - - reporter.printErrors(summary); - - expect(infoStub).to.have.been.calledWithMatch("Unable to set the invoker for the IAM policy"); - expect(infoStub).to.not.have.been.calledWithMatch( - "One or more functions were being implicitly made publicly available" - ); - - infoStub.resetHistory(); - // No longer explicitly setting invoker - summary.results[0].endpoint = ENDPOINT; - reporter.printErrors(summary); - - expect(infoStub).to.have.been.calledWithMatch("Unable to set the invoker for the IAM policy"); - expect(infoStub).to.have.been.calledWithMatch( - "One or more functions were being implicitly made publicly available" - ); - }); - - it("prints quota errors", () => { - const rawError = new Error("Quota exceeded"); - (rawError as any).status = 429; - const summary: reporter.Summary = { - totalTime: 1_000, - results: [ - { - endpoint: ENDPOINT, - durationMs: 1_000, - error: new reporter.DeploymentError(ENDPOINT, "create", rawError), - }, - ], - }; - - reporter.printErrors(summary); - expect(infoStub).to.have.been.calledWithMatch( - "Exceeded maximum retries while deploying functions." - ); - }); - - it("prints aborted errors", () => { - const summary: reporter.Summary = { - totalTime: 1_000, - results: [ - { - endpoint: { ...ENDPOINT, id: "failedCreate" }, - durationMs: 1_000, - error: new reporter.DeploymentError(ENDPOINT, "create", undefined), - }, - { - endpoint: { ...ENDPOINT, id: "abortedDelete" }, - durationMs: 1_000, - error: new reporter.AbortedDeploymentError(ENDPOINT), - }, - ], - }; - - reporter.printErrors(summary); - expect(infoStub).to.have.been.calledWithMatch( - /the following functions were not deleted.*abortedDelete/s - ); - expect(infoStub).to.not.have.been.calledWith( - /the following functions were not deleted.*failedCreate/s - ); - }); - }); -}); diff --git a/src/test/deploy/functions/release/sourceTokenScraper.spec.ts b/src/test/deploy/functions/release/sourceTokenScraper.spec.ts deleted file mode 100644 index 40927a0b25c..00000000000 --- a/src/test/deploy/functions/release/sourceTokenScraper.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect } from "chai"; - -import { SourceTokenScraper } from "../../../../deploy/functions/release/sourceTokenScraper"; - -describe("SourcTokenScraper", () => { - it("immediately provides the first result", async () => { - const scraper = new SourceTokenScraper(); - await expect(scraper.tokenPromise()).to.eventually.be.undefined; - }); - - it("provides results after the firt operation completes", async () => { - const scraper = new SourceTokenScraper(); - // First result comes right away; - await expect(scraper.tokenPromise()).to.eventually.be.undefined; - - let gotResult = false; - const timeout = new Promise((resolve, reject) => { - setTimeout(() => reject(new Error("Timeout")), 10); - }); - const getResult = (async () => { - await scraper.tokenPromise(); - gotResult = true; - })(); - await expect(Promise.race([getResult, timeout])).to.be.rejectedWith("Timeout"); - expect(gotResult).to.be.false; - - scraper.poller({ done: true }); - await expect(getResult).to.eventually.be.undefined; - }); - - it("provides tokens from an operation", async () => { - const scraper = new SourceTokenScraper(); - // First result comes right away - await expect(scraper.tokenPromise()).to.eventually.be.undefined; - - scraper.poller({ - metadata: { - sourceToken: "magic token", - target: "projects/p/locations/l/functions/f", - }, - }); - await expect(scraper.tokenPromise()).to.eventually.equal("magic token"); - }); -}); diff --git a/src/test/deploy/functions/runtimes/discovery/index.spec.ts b/src/test/deploy/functions/runtimes/discovery/index.spec.ts deleted file mode 100644 index 9279f990a88..00000000000 --- a/src/test/deploy/functions/runtimes/discovery/index.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { expect } from "chai"; -import * as yaml from "js-yaml"; -import * as sinon from "sinon"; -import * as nock from "nock"; - -import * as api from "../../../../../api"; -import { FirebaseError } from "../../../../../error"; -import * as discovery from "../../../../../deploy/functions/runtimes/discovery"; -import * as backend from "../../../../../deploy/functions/backend"; - -const MIN_ENDPOINT = { - entryPoint: "entrypoint", - httpsTrigger: {}, -}; - -const ENDPOINT: backend.Endpoint = { - ...MIN_ENDPOINT, - id: "id", - platform: "gcfv2", - project: "project", - region: api.functionsDefaultRegion, - runtime: "nodejs16", -}; - -const YAML_OBJ = { - specVersion: "v1alpha1", - endpoints: { id: MIN_ENDPOINT }, -}; - -const YAML_TEXT = yaml.dump(YAML_OBJ); - -const BACKEND: backend.Backend = backend.of(ENDPOINT); - -describe("yamlToBackend", () => { - it("Accepts a valid v1alpha1 spec", () => { - const parsed = discovery.yamlToBackend( - YAML_OBJ, - "project", - api.functionsDefaultRegion, - "nodejs16" - ); - expect(parsed).to.deep.equal(BACKEND); - }); - - it("Requires a spec version", () => { - const flawed: Record = { ...YAML_OBJ }; - delete flawed.specVersion; - expect(() => - discovery.yamlToBackend(flawed, "project", api.functionsDefaultRegion, "nodejs16") - ).to.throw(FirebaseError); - }); - - it("Throws on unknown spec versions", () => { - const flawed = { - ...YAML_OBJ, - specVersion: "32767beta2", - }; - expect(() => - discovery.yamlToBackend(flawed, "project", api.functionsDefaultRegion, "nodejs16") - ).to.throw(FirebaseError); - }); -}); - -describe("detectFromYaml", () => { - let readFileAsync: sinon.SinonStub; - - beforeEach(() => { - readFileAsync = sinon.stub(discovery, "readFileAsync"); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - it("succeeds when YAML can be found", async () => { - readFileAsync.resolves(YAML_TEXT); - - await expect( - discovery.detectFromYaml("directory", "project", "nodejs16") - ).to.eventually.deep.equal(BACKEND); - }); - - it("returns undefined when YAML cannot be found", async () => { - readFileAsync.rejects({ code: "ENOENT" }); - - await expect(discovery.detectFromYaml("directory", "project", "nodejs16")).to.eventually.equal( - undefined - ); - }); -}); - -describe("detectFromPort", () => { - afterEach(() => { - nock.cleanAll(); - }); - - // This test requires us to launch node and load express.js. On my 16" MBP this takes - // 600ms, which is dangerously close to the default limit of 1s. Increase limits so - // that this doesn't flake even when running on slower machines experiencing hiccup - it("passes as smoke test", async () => { - nock("http://localhost:8080").get("/backend.yaml").times(20).replyWithError({ - message: "Still booting", - code: "ECONNREFUSED", - }); - - nock("http://localhost:8080").get("/backend.yaml").reply(200, YAML_TEXT); - - const parsed = await discovery.detectFromPort(8080, "project", "nodejs16"); - expect(parsed).to.deep.equal(BACKEND); - }); -}); diff --git a/src/test/deploy/functions/runtimes/discovery/parsing.spec.ts b/src/test/deploy/functions/runtimes/discovery/parsing.spec.ts deleted file mode 100644 index 9f34cba0b68..00000000000 --- a/src/test/deploy/functions/runtimes/discovery/parsing.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { expect } from "chai"; -import { FirebaseError } from "../../../../../error"; -import * as parsing from "../../../../../deploy/functions/runtimes/discovery/parsing"; - -describe("requireKeys", () => { - it("accepts found keys", () => { - const obj = { - foo: "foo", - bar: "bar", - }; - parsing.requireKeys("", obj, "foo", "bar"); - }); - - it("throws for missing keys", () => { - const obj = { - foo: "foo", - } as any; - expect(() => parsing.requireKeys("", obj, "foo", "bar")).to.throw( - FirebaseError, - "Expected key bar" - ); - }); - - it("uses prefixes in error messages", () => { - const obj = { - foo: { - bar: 1, - }, - } as any; - expect(() => parsing.requireKeys("foo", obj.foo, "baz")).to.throw( - FirebaseError, - "Expected key foo.baz" - ); - }); -}); - -describe("assertKeyTypes", () => { - const tests = ["string", "number", "boolean", "array", "object"]; - const values = { - null: null, - undefined: undefined, - number: 0, - boolean: false, - string: "", - array: [], - object: {}, - }; - for (const type of tests) { - const schema = { [type]: type as parsing.KeyType }; - for (const [testType, val] of Object.entries(values)) { - it(`handles a ${testType} when expecting a ${type}`, () => { - const obj = { [type]: val }; - if (type === testType) { - expect(() => parsing.assertKeyTypes("", obj, schema)).not.to.throw; - } else { - expect(() => parsing.assertKeyTypes("", obj, schema)).to.throw(FirebaseError); - } - }); - } - } - - it("Throws on superfluous keys", () => { - const obj = { foo: "bar", number: 1 } as any; - expect(() => - parsing.assertKeyTypes("", obj, { - foo: "string", - }) - ).to.throw(FirebaseError, /Unexpected key number/); - }); - - // Omit isn't really useful at runtime, but it allows us to - // use the type system to declare that all fields of T have - // a corresponding schema type. - it("Ignores 'omit' keys", () => { - const obj = { foo: "bar", number: 1 } as any; - expect(() => - parsing.assertKeyTypes("", obj, { - foo: "string", - number: "omit", - }) - ); - }); - - it("Handles prefixes", () => { - const obj = { - foo: {}, - } as any; - expect(() => - parsing.assertKeyTypes("outer", obj, { - foo: "array", - }) - ).to.throw(FirebaseError, "Expected outer.foo to be an array"); - }); -}); diff --git a/src/test/deploy/functions/runtimes/discovery/v1alpha1.spec.ts b/src/test/deploy/functions/runtimes/discovery/v1alpha1.spec.ts deleted file mode 100644 index 58e37020f89..00000000000 --- a/src/test/deploy/functions/runtimes/discovery/v1alpha1.spec.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { expect } from "chai"; - -import { FirebaseError } from "../../../../../error"; -import * as backend from "../../../../../deploy/functions/backend"; -import { Runtime } from "../../../../../deploy/functions/runtimes"; -import * as v1alpha1 from "../../../../../deploy/functions/runtimes/discovery/v1alpha1"; - -const PROJECT = "project"; -const REGION = "region"; -const RUNTIME: Runtime = "node14"; -const MIN_ENDPOINT: Omit = { - entryPoint: "entryPoint", -}; - -describe("backendFromV1Alpha1", () => { - describe("parser errors", () => { - function assertParserError(obj: unknown): void { - expect(() => v1alpha1.backendFromV1Alpha1(obj, PROJECT, REGION, RUNTIME)).to.throw( - FirebaseError - ); - } - - describe("backend keys", () => { - it("throws on the empty object", () => { - assertParserError({}); - }); - - const invalidBackendTypes = { - requiredAPIS: ["cloudscheduler.googleapis.com"], - endpoints: [], - }; - for (const [key, value] of Object.entries(invalidBackendTypes)) { - it(`throws on invalid value for top-level key ${key}`, () => { - const obj = { - requiredAPIs: {}, - endpoints: {}, - [key]: value, - }; - assertParserError(obj); - }); - } - - it("throws on unknown keys", () => { - assertParserError({ eventArcTriggers: [] }); - }); - }); // top level keys - - describe("Endpoint keys", () => { - it("invalid keys", () => { - assertParserError({ - endpoints: { - id: { - ...MIN_ENDPOINT, - httpsTrigger: {}, - invalid: "key", - }, - }, - }); - }); - - for (const key of Object.keys(MIN_ENDPOINT)) { - it(`missing Endpoint key ${key}`, () => { - const func = { ...MIN_ENDPOINT, httpsTrigger: {} } as Record; - delete func[key]; - assertParserError({ cloudFunctions: [func] }); - }); - } - - const invalidFunctionEntries = { - platform: 2, - id: 1, - region: "us-central1", - project: 42, - runtime: null, - entryPoint: 5, - availableMemoryMb: "2GB", - maxInstances: "2", - minInstances: "1", - serviceAccountEmail: { ldap: "inlined" }, - timeout: 60, - trigger: [], - vpcConnector: 2, - vpcConnectorEgressSettings: {}, - labels: "yes", - ingressSettings: true, - }; - for (const [key, value] of Object.entries(invalidFunctionEntries)) { - it(`invalid value for CloudFunction key ${key}`, () => { - const endpoint = { - ...MIN_ENDPOINT, - httpsTrigger: {}, - [key]: value, - }; - assertParserError({ endpoints: { endpoint } }); - }); - } - }); // Top level function keys - - describe("Event triggers", () => { - const validTrigger: backend.EventTrigger = { - eventType: "google.pubsub.v1.topic.publish", - eventFilters: { resource: "projects/p/topics/t" }, - retry: true, - region: "global", - serviceAccountEmail: "root@", - }; - for (const key of ["eventType", "eventFilters"]) { - it(`missing event trigger key ${key}`, () => { - const eventTrigger = { ...validTrigger } as Record; - delete eventTrigger[key]; - assertParserError({ - endpoints: { - func: { ...MIN_ENDPOINT, eventTrigger }, - }, - }); - }); - } - - const invalidEntries = { - eventType: { foo: "bar" }, - eventFilters: 42, - retry: {}, - region: ["us-central1"], - serviceAccountEmail: ["ldap"], - }; - for (const [key, value] of Object.entries(invalidEntries)) { - it(`invalid value for event trigger key ${key}`, () => { - const eventTrigger = { - ...validTrigger, - [key]: value, - }; - assertParserError({ - endpoints: { - func: { ...MIN_ENDPOINT, eventTrigger }, - }, - }); - }); - } - }); // Event triggers - - describe("httpsTriggers", () => { - it("invalid value for https trigger key invoker", () => { - assertParserError({ - endpoints: { - func: { - ...MIN_ENDPOINT, - httpsTrigger: { invoker: 42 }, - }, - }, - }); - }); - }); - - describe("scheduleTriggers", () => { - const validTrigger: backend.ScheduleTrigger = { - schedule: "every 5 minutes", - timeZone: "America/Los_Angeles", - retryConfig: { - retryCount: 42, - minBackoffDuration: "1s", - maxBackoffDuration: "20s", - maxDoublings: 20, - maxRetryDuration: "120s", - }, - }; - - const invalidEntries = { - schedule: 46, - timeZone: {}, - }; - for (const [key, value] of Object.entries(invalidEntries)) { - it(`invalid value for schedule trigger key ${key}`, () => { - const scheduleTrigger = { - ...validTrigger, - [key]: value, - }; - assertParserError({ - endpoints: { - func: { ...MIN_ENDPOINT, scheduleTrigger }, - }, - }); - }); - } - - const invalidRetryEntries = { - retryCount: "42", - minBackoffDuration: 1, - maxBackoffDuration: 20, - maxDoublings: "20", - maxRetryDuration: 120, - }; - for (const [key, value] of Object.entries(invalidRetryEntries)) { - const retryConfig = { - ...validTrigger.retryConfig, - [key]: value, - }; - const scheduleTrigger = { ...validTrigger, retryConfig }; - assertParserError({ - endpoints: { - func: { ...MIN_ENDPOINT, scheduleTrigger }, - }, - }); - } - }); - - describe("taskQueueTriggers", () => { - const validTrigger: backend.TaskQueueTrigger = { - rateLimits: { - maxBurstSize: 5, - maxConcurrentDispatches: 10, - maxDispatchesPerSecond: 20, - }, - retryConfig: { - maxAttempts: 3, - maxRetryDuration: "120s", - minBackoff: "1s", - maxBackoff: "30s", - maxDoublings: 5, - }, - invoker: ["custom@"], - }; - - const invalidRateLimits = { - maxBurstSize: "5", - maxConcurrentDispatches: "10", - maxDispatchesPerSecond: "20", - }; - for (const [key, value] of Object.entries(invalidRateLimits)) { - const rateLimits = { - ...validTrigger.rateLimits, - [key]: value, - }; - const taskQueueTrigger = { ...validTrigger, rateLimits }; - assertParserError({ - endpoints: { - func: { ...MIN_ENDPOINT, taskQueueTrigger }, - }, - }); - } - - const invalidRetryConfigs = { - maxAttempts: "3", - maxRetryDuration: 120, - minBackoff: 1, - maxBackoff: 30, - maxDoublings: "5", - }; - for (const [key, value] of Object.entries(invalidRetryConfigs)) { - const retryConfig = { - ...validTrigger.retryConfig, - [key]: value, - }; - const taskQueueTrigger = { ...validTrigger, retryConfig }; - assertParserError({ - endpoints: { - func: { ...MIN_ENDPOINT, taskQueueTrigger }, - }, - }); - } - }); - - it("detects missing triggers", () => { - assertParserError({ endpoints: MIN_ENDPOINT }); - }); - }); // Parser errors; - - describe("allows valid backends", () => { - const DEFAULTED_ENDPOINT: Omit = { - ...MIN_ENDPOINT, - platform: "gcfv2", - id: "id", - project: PROJECT, - region: REGION, - runtime: RUNTIME, - }; - - it("fills default backend and function fields", () => { - const yaml: v1alpha1.Manifest = { - specVersion: "v1alpha1", - endpoints: { - id: { - ...MIN_ENDPOINT, - httpsTrigger: {}, - }, - }, - }; - const parsed = v1alpha1.backendFromV1Alpha1(yaml, PROJECT, REGION, RUNTIME); - const expected: backend.Backend = backend.of({ ...DEFAULTED_ENDPOINT, httpsTrigger: {} }); - expect(parsed).to.deep.equal(expected); - }); - - it("copies schedules", () => { - const scheduleTrigger: backend.ScheduleTrigger = { - schedule: "every 5 minutes", - timeZone: "America/Los_Angeles", - retryConfig: { - retryCount: 20, - minBackoffDuration: "1s", - maxBackoffDuration: "20s", - maxRetryDuration: "120s", - maxDoublings: 10, - }, - }; - - const yaml: v1alpha1.Manifest = { - specVersion: "v1alpha1", - endpoints: { - id: { - ...MIN_ENDPOINT, - scheduleTrigger, - }, - }, - }; - const expected = backend.of({ ...DEFAULTED_ENDPOINT, scheduleTrigger }); - const parsed = v1alpha1.backendFromV1Alpha1(yaml, PROJECT, REGION, RUNTIME); - expect(parsed).to.deep.equal(expected); - }); - - it("copies event triggers", () => { - const eventTrigger: backend.EventTrigger = { - eventType: "google.pubsub.topic.v1.publish", - eventFilters: { - resource: "projects/project/topics/topic", - }, - region: "us-central1", - serviceAccountEmail: "sa@", - retry: true, - }; - const yaml: v1alpha1.Manifest = { - specVersion: "v1alpha1", - endpoints: { - id: { - ...MIN_ENDPOINT, - eventTrigger, - }, - }, - }; - const expected = backend.of({ ...DEFAULTED_ENDPOINT, eventTrigger }); - const parsed = v1alpha1.backendFromV1Alpha1(yaml, PROJECT, REGION, RUNTIME); - expect(parsed).to.deep.equal(expected); - }); - - it("copies optional fields", () => { - const fields: backend.ServiceConfiguration = { - concurrency: 42, - labels: { hello: "world" }, - environmentVariables: { foo: "bar" }, - availableMemoryMb: 256, - timeout: "60s", - maxInstances: 20, - minInstances: 1, - vpcConnector: "hello", - vpcConnectorEgressSettings: "ALL_TRAFFIC", - ingressSettings: "ALLOW_INTERNAL_ONLY", - serviceAccountEmail: "sa@", - }; - - const yaml: v1alpha1.Manifest = { - specVersion: "v1alpha1", - endpoints: { - id: { - ...MIN_ENDPOINT, - httpsTrigger: {}, - ...fields, - }, - }, - }; - const expected = backend.of({ - ...DEFAULTED_ENDPOINT, - httpsTrigger: {}, - ...fields, - }); - const parsed = v1alpha1.backendFromV1Alpha1(yaml, PROJECT, REGION, RUNTIME); - expect(parsed).to.deep.equal(expected); - }); - - it("handles multiple regions", () => { - const yaml: v1alpha1.Manifest = { - specVersion: "v1alpha1", - endpoints: { - id: { - ...MIN_ENDPOINT, - httpsTrigger: {}, - region: ["region1", "region2"], - }, - }, - }; - const expected = backend.of( - { - ...DEFAULTED_ENDPOINT, - httpsTrigger: {}, - region: "region1", - }, - { - ...DEFAULTED_ENDPOINT, - httpsTrigger: {}, - region: "region2", - } - ); - const parsed = v1alpha1.backendFromV1Alpha1(yaml, PROJECT, REGION, RUNTIME); - expect(parsed).to.deep.equal(expected); - }); - }); -}); diff --git a/src/test/deploy/functions/runtimes/golang/gomod.spec.ts b/src/test/deploy/functions/runtimes/golang/gomod.spec.ts deleted file mode 100644 index e2e61d62e36..00000000000 --- a/src/test/deploy/functions/runtimes/golang/gomod.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect } from "chai"; -import * as gomod from "../../../../../deploy/functions/runtimes/golang/gomod"; -import * as go from "../../../../../deploy/functions/runtimes/golang"; - -const MOD_NAME = "acme.com/fucntions"; -const GO_VERSION = "1.13"; -const FUNCTIONS_MOD = "firebase.google.com/firebase-functions-go"; -const MIN_MODULE = `module ${MOD_NAME} - -go ${GO_VERSION} -`; - -const INLINE_MODULE = `${MIN_MODULE} -require ${go.ADMIN_SDK} v4.6.0 // indirect - -replace ${FUNCTIONS_MOD} => ${go.FUNCTIONS_SDK} -`; - -const BLOCK_MODULE = `${MIN_MODULE} - -require ( - ${go.ADMIN_SDK} v4.6.0 // indirect -) - -replace ( - ${FUNCTIONS_MOD} => ${go.FUNCTIONS_SDK} -) -`; - -describe("Modules", () => { - it("Should parse a bare minimum module", () => { - const mod = gomod.parseModule(MIN_MODULE); - expect(mod.module).to.equal(MOD_NAME); - expect(mod.version).to.equal(GO_VERSION); - }); - - it("Should parse inline statements", () => { - const mod = gomod.parseModule(INLINE_MODULE); - expect(mod.module).to.equal(MOD_NAME); - expect(mod.version).to.equal(GO_VERSION); - expect(mod.dependencies).to.deep.equal({ - [go.ADMIN_SDK]: "v4.6.0", - }); - expect(mod.replaces).to.deep.equal({ - [FUNCTIONS_MOD]: go.FUNCTIONS_SDK, - }); - }); - - it("Should parse block statements", () => { - const mod = gomod.parseModule(BLOCK_MODULE); - expect(mod.module).to.equal(MOD_NAME); - expect(mod.version).to.equal(GO_VERSION); - expect(mod.dependencies).to.deep.equal({ - [go.ADMIN_SDK]: "v4.6.0", - }); - expect(mod.replaces).to.deep.equal({ - [FUNCTIONS_MOD]: go.FUNCTIONS_SDK, - }); - }); -}); diff --git a/src/test/deploy/functions/runtimes/index.spec.ts b/src/test/deploy/functions/runtimes/index.spec.ts deleted file mode 100644 index 460783f733b..00000000000 --- a/src/test/deploy/functions/runtimes/index.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect } from "chai"; - -import * as runtimes from "../../../../deploy/functions/runtimes"; - -describe("getHumanFriendlyRuntimeName", () => { - it("should properly convert raw runtime to human friendly runtime", () => { - expect(runtimes.getHumanFriendlyRuntimeName("nodejs6")).to.contain("Node.js"); - }); -}); diff --git a/src/test/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.spec.ts b/src/test/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.spec.ts deleted file mode 100644 index 2980980877f..00000000000 --- a/src/test/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -// Have to disable this because no @types/cjson available -// eslint-disable-next-line -const cjson = require("cjson"); - -import { FirebaseError } from "../../../../../error"; -import * as runtime from "../../../../../deploy/functions/runtimes/node/parseRuntimeAndValidateSDK"; - -describe("getRuntimeChoice", () => { - const sandbox = sinon.createSandbox(); - let cjsonStub: sinon.SinonStub; - let SDKVersionStub: sinon.SinonStub; - - beforeEach(() => { - cjsonStub = sandbox.stub(cjson, "load"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - context("when the runtime is set in firebase.json", () => { - it("should error if runtime field is set to node 6", () => { - expect(() => { - runtime.getRuntimeChoice("path/to/source", "nodejs6"); - }).to.throw(runtime.UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG); - }); - - it("should error if runtime field is set to node 8", () => { - expect(() => { - runtime.getRuntimeChoice("path/to/source", "nodejs8"); - }).to.throw(runtime.UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG); - }); - - it("should return node 10 if runtime field is set to node 10", () => { - expect(runtime.getRuntimeChoice("path/to/source", "nodejs10")).to.equal("nodejs10"); - }); - - it("should return node 12 if runtime field is set to node 12", () => { - expect(runtime.getRuntimeChoice("path/to/source", "nodejs12")).to.equal("nodejs12"); - }); - - it("should return node 14 if runtime field is set to node 14", () => { - expect(runtime.getRuntimeChoice("path/to/source", "nodejs14")).to.equal("nodejs14"); - }); - - it("should return node 16 if runtime field is set to node 16", () => { - expect(runtime.getRuntimeChoice("path/to/source", "nodejs16")).to.equal("nodejs16"); - }); - - it("should throw error if unsupported node version set", () => { - expect(() => runtime.getRuntimeChoice("path/to/source", "nodejs11")).to.throw( - FirebaseError, - runtime.UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG - ); - }); - }); - - context("when the runtime is not set in firebase.json", () => { - it("should error if engines field is set to node 6", () => { - cjsonStub.returns({ engines: { node: "6" } }); - - expect(() => { - runtime.getRuntimeChoice("path/to/source", ""); - }).to.throw(runtime.UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG); - }); - - it("should error if engines field is set to node 8", () => { - cjsonStub.returns({ engines: { node: "8" } }); - - expect(() => { - runtime.getRuntimeChoice("path/to/source", ""); - }).to.throw(runtime.UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG); - }); - - it("should return node 10 if engines field is set to node 10", () => { - cjsonStub.returns({ engines: { node: "10" } }); - - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs10"); - }); - - it("should return node 12 if engines field is set to node 12", () => { - cjsonStub.returns({ engines: { node: "12" } }); - - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs12"); - }); - - it("should return node 14 if engines field is set to node 14", () => { - cjsonStub.returns({ engines: { node: "14" } }); - - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs14"); - }); - - it("should return node 16 if engines field is set to node 16", () => { - cjsonStub.returns({ engines: { node: "16" } }); - - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs16"); - }); - - it("should print warning when firebase-functions version is below 2.0.0", () => { - cjsonStub.returns({ engines: { node: "16" } }); - - runtime.getRuntimeChoice("path/to/source", ""); - }); - - it("should not throw error if user's SDK version fails to be fetched", () => { - cjsonStub.returns({ engines: { node: "10" } }); - // Intentionally not setting SDKVersionStub so it can fail to be fetched. - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs10"); - }); - - it("should throw error if unsupported node version set", () => { - cjsonStub.returns({ - engines: { node: "11" }, - }); - expect(() => runtime.getRuntimeChoice("path/to/source", "")).to.throw( - FirebaseError, - runtime.UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG - ); - }); - }); -}); diff --git a/src/test/deploy/functions/runtimes/node/parseTriggers.spec.ts b/src/test/deploy/functions/runtimes/node/parseTriggers.spec.ts deleted file mode 100644 index e148de92a52..00000000000 --- a/src/test/deploy/functions/runtimes/node/parseTriggers.spec.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { expect } from "chai"; - -import { FirebaseError } from "../../../../../error"; -import * as backend from "../../../../../deploy/functions/backend"; -import * as parseTriggers from "../../../../../deploy/functions/runtimes/node/parseTriggers"; -import * as api from "../../../../../api"; - -describe("addResourcesToBackend", () => { - const oldDefaultRegion = api.functionsDefaultRegion; - before(() => { - (api as any).functionsDefaultRegion = "us-central1"; - }); - - after(() => { - (api as any).functionsDefaultRegion = oldDefaultRegion; - }); - - const BASIC_TRIGGER: parseTriggers.TriggerAnnotation = Object.freeze({ - name: "func", - entryPoint: "func", - }); - - const BASIC_FUNCTION_NAME: backend.TargetIds = Object.freeze({ - id: "func", - region: api.functionsDefaultRegion, - project: "project", - }); - - const BASIC_ENDPOINT: Omit = Object.freeze({ - platform: "gcfv1", - ...BASIC_FUNCTION_NAME, - runtime: "nodejs16", - entryPoint: "func", - }); - - it("should assert against impossible configurations", () => { - expect(() => { - parseTriggers.addResourcesToBackend( - "project", - "nodejs16", - { - ...BASIC_TRIGGER, - httpsTrigger: {}, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "projects/project/topics/topic", - service: "pubsub.googleapis.com", - }, - }, - backend.empty() - ); - }).to.throw(FirebaseError); - }); - - it("should handle a minimal https trigger", () => { - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - httpsTrigger: {}, - }; - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const expected: backend.Backend = backend.of({ ...BASIC_ENDPOINT, httpsTrigger: {} }); - expect(result).to.deep.equal(expected); - }); - - it("should handle a minimal task queue trigger", () => { - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - taskQueueTrigger: {}, - }; - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const expected: backend.Backend = { - ...backend.of({ ...BASIC_ENDPOINT, taskQueueTrigger: {} }), - requiredAPIs: { - cloudtasks: "cloudtasks.googleapis.com", - }, - }; - expect(result).to.deep.equal(expected); - }); - - describe("should handle a minimal event trigger", () => { - for (const failurePolicy of [undefined, false, true, { retry: {} }]) { - const name = - typeof failurePolicy === "undefined" ? "undefined" : JSON.stringify(failurePolicy); - it(`should handle failurePolicy=${name}`, () => { - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - eventTrigger: { - service: "pubsub.googleapis.com", - eventType: "google.pubsub.topic.publish", - resource: "projects/project/topics/topic", - }, - }; - if (typeof failurePolicy !== "undefined") { - trigger.failurePolicy = failurePolicy; - } - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const eventTrigger: backend.EventTrigger = { - eventType: "google.pubsub.topic.publish", - eventFilters: { - resource: "projects/project/topics/topic", - }, - retry: !!failurePolicy, - }; - const expected: backend.Backend = backend.of({ ...BASIC_ENDPOINT, eventTrigger }); - expect(result).to.deep.equal(expected); - }); - } - }); - - it("should copy fields", () => { - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - httpsTrigger: { - invoker: ["public"], - }, - maxInstances: 42, - minInstances: 1, - serviceAccountEmail: "inlined@google.com", - vpcConnectorEgressSettings: "PRIVATE_RANGES_ONLY", - vpcConnector: "projects/project/locations/region/connectors/connector", - ingressSettings: "ALLOW_ALL", - timeout: "60s", - labels: { - test: "testing", - }, - }; - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const config: backend.ServiceConfiguration = { - maxInstances: 42, - minInstances: 1, - serviceAccountEmail: "inlined@google.com", - vpcConnectorEgressSettings: "PRIVATE_RANGES_ONLY", - vpcConnector: "projects/project/locations/region/connectors/connector", - ingressSettings: "ALLOW_ALL", - timeout: "60s", - labels: { - test: "testing", - }, - }; - const expected: backend.Backend = backend.of({ - ...BASIC_ENDPOINT, - httpsTrigger: { - invoker: ["public"], - }, - ...config, - }); - expect(result).to.deep.equal(expected); - }); - - it("should rename/transform fields", () => { - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "projects/p/topics/t", - service: "pubsub.googleapis.com", - }, - }; - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const eventTrigger: backend.EventTrigger = { - eventType: "google.pubsub.topic.publish", - eventFilters: { - resource: "projects/p/topics/t", - }, - retry: false, - }; - - const expected: backend.Backend = backend.of({ ...BASIC_ENDPOINT, eventTrigger }); - expect(result).to.deep.equal(expected); - }); - - it("should support explicit regions", () => { - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - httpsTrigger: {}, - regions: ["europe-west1"], - }; - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const expected: backend.Backend = backend.of({ - ...BASIC_ENDPOINT, - region: "europe-west1", - httpsTrigger: {}, - }); - expect(result).to.deep.equal(expected); - }); - - it("should support multiple regions", () => { - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - httpsTrigger: {}, - regions: ["us-central1", "europe-west1"], - }; - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const expected: backend.Backend = backend.of( - { - ...BASIC_ENDPOINT, - httpsTrigger: {}, - region: "us-central1", - }, - { - ...BASIC_ENDPOINT, - httpsTrigger: {}, - region: "europe-west1", - } - ); - - expect(result).to.deep.equal(expected); - }); - - it("should support schedules", () => { - const schedule = { - schedule: "every 10 minutes", - timeZone: "America/Los_Angeles", - retryConfig: { - retryCount: 20, - maxRetryDuration: "200s", - minBackoffDuration: "1s", - maxBackoffDuration: "10s", - maxDoublings: 10, - }, - }; - const trigger: parseTriggers.TriggerAnnotation = { - ...BASIC_TRIGGER, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "projects/project/topics", - service: "pubsub.googleapis.com", - }, - regions: ["us-central1", "europe-west1"], - schedule, - labels: { - test: "testing", - }, - }; - - const result = backend.empty(); - parseTriggers.addResourcesToBackend("project", "nodejs16", trigger, result); - - const europeFunctionName = { - ...BASIC_FUNCTION_NAME, - region: "europe-west1", - }; - - const expected: backend.Backend = { - ...backend.of( - { - ...BASIC_ENDPOINT, - region: "us-central1", - labels: { - test: "testing", - }, - scheduleTrigger: schedule, - }, - { - ...BASIC_ENDPOINT, - region: "europe-west1", - labels: { - test: "testing", - }, - scheduleTrigger: schedule, - } - ), - requiredAPIs: { - pubsub: "pubsub.googleapis.com", - scheduler: "cloudscheduler.googleapis.com", - }, - }; - - expect(result).to.deep.equal(expected); - }); -}); diff --git a/src/test/deploy/functions/runtimes/node/validate.spec.ts b/src/test/deploy/functions/runtimes/node/validate.spec.ts deleted file mode 100644 index dba30e61e5b..00000000000 --- a/src/test/deploy/functions/runtimes/node/validate.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../../../../error"; -import { RUNTIME_NOT_SET } from "../../../../../deploy/functions/runtimes/node/parseRuntimeAndValidateSDK"; -import * as validate from "../../../../../deploy/functions/runtimes/node/validate"; -import * as fsutils from "../../../../../fsutils"; - -const cjson = require("cjson"); - -describe("validate", () => { - describe("packageJsonIsValid", () => { - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let cjsonLoadStub: sinon.SinonStub; - let fileExistsStub: sinon.SinonStub; - - beforeEach(() => { - fileExistsStub = sandbox.stub(fsutils, "fileExistsSync"); - cjsonLoadStub = sandbox.stub(cjson, "load"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should throw error if package.json file is missing", () => { - fileExistsStub.withArgs("sourceDir/package.json").returns(false); - - expect(() => { - validate.packageJsonIsValid("sourceDirName", "sourceDir", "projectDir"); - }).to.throw(FirebaseError, "No npm package found"); - }); - - it("should throw error if functions source file is missing", () => { - cjsonLoadStub.returns({ name: "my-project", engines: { node: "8" } }); - fileExistsStub.withArgs("sourceDir/package.json").returns(true); - fileExistsStub.withArgs("sourceDir/index.js").returns(false); - - expect(() => { - validate.packageJsonIsValid("sourceDirName", "sourceDir", "projectDir"); - }).to.throw(FirebaseError, "does not exist, can't deploy"); - }); - - it("should throw error if main is defined and that file is missing", () => { - cjsonLoadStub.returns({ name: "my-project", main: "src/main.js", engines: { node: "8" } }); - fileExistsStub.withArgs("sourceDir/package.json").returns(true); - fileExistsStub.withArgs("sourceDir/src/main.js").returns(false); - - expect(() => { - validate.packageJsonIsValid("sourceDirName", "sourceDir", "projectDir"); - }).to.throw(FirebaseError, "does not exist, can't deploy"); - }); - - it("should not throw error if runtime is set in the config and the engines field is not set", () => { - cjsonLoadStub.returns({ name: "my-project" }); - fileExistsStub.withArgs("sourceDir/package.json").returns(true); - fileExistsStub.withArgs("sourceDir/index.js").returns(true); - - expect(() => { - validate.packageJsonIsValid("sourceDirName", "sourceDir", "projectDir"); - }).to.not.throw(); - }); - }); -}); diff --git a/src/test/deploy/functions/services/storage.spec.ts b/src/test/deploy/functions/services/storage.spec.ts deleted file mode 100644 index f088ae66c2a..00000000000 --- a/src/test/deploy/functions/services/storage.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import { obtainStorageBindings } from "../../../../deploy/functions/services/storage"; -import * as storage from "../../../../gcp/storage"; - -const STORAGE_RES = { - email_address: "service-123@gs-project-accounts.iam.gserviceaccount.com", - kind: "storage#serviceAccount", -}; - -const BINDING = { - role: "some/role", - members: ["someuser"], -}; - -describe("obtainStorageBindings", () => { - let storageStub: sinon.SinonStub; - - beforeEach(() => { - storageStub = sinon - .stub(storage, "getServiceAccount") - .throws("unexpected call to storage.getServiceAccount"); - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - it("should return pubsub binding when missing from the policy", async () => { - storageStub.resolves(STORAGE_RES); - const existingPolicy = { - etag: "etag", - version: 3, - bindings: [BINDING], - }; - - const bindings = await obtainStorageBindings("project", existingPolicy); - - expect(bindings.length).to.equal(1); - expect(bindings[0]).to.deep.equal({ - role: "roles/pubsub.publisher", - members: [`serviceAccount:${STORAGE_RES.email_address}`], - }); - }); - - it("should return the updated pubsub binding from the policy", async () => { - storageStub.resolves(STORAGE_RES); - const existingPolicy = { - etag: "etag", - version: 3, - bindings: [BINDING, { role: "roles/pubsub.publisher", members: ["someuser"] }], - }; - - const bindings = await obtainStorageBindings("project", existingPolicy); - - expect(bindings.length).to.equal(1); - expect(bindings[0]).to.deep.equal({ - role: "roles/pubsub.publisher", - members: ["someuser", `serviceAccount:${STORAGE_RES.email_address}`], - }); - }); - - it("should return the binding from policy if already present", async () => { - storageStub.resolves(STORAGE_RES); - const existingPolicy = { - etag: "etag", - version: 3, - bindings: [ - BINDING, - { - role: "roles/pubsub.publisher", - members: [`serviceAccount:${STORAGE_RES.email_address}`], - }, - ], - }; - - const bindings = await obtainStorageBindings("project", existingPolicy); - expect(bindings.length).to.equal(1); - expect(bindings[0]).to.deep.equal({ - role: "roles/pubsub.publisher", - members: [`serviceAccount:${STORAGE_RES.email_address}`], - }); - }); -}); diff --git a/src/test/deploy/functions/validate.spec.ts b/src/test/deploy/functions/validate.spec.ts deleted file mode 100644 index 5a2d3e6a1b7..00000000000 --- a/src/test/deploy/functions/validate.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../../error"; -import * as fsutils from "../../../fsutils"; -import * as validate from "../../../deploy/functions/validate"; -import * as projectPath from "../../../projectPath"; - -describe("validate", () => { - describe("functionsDirectoryExists", () => { - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let resolvePpathStub: sinon.SinonStub; - let dirExistsStub: sinon.SinonStub; - - beforeEach(() => { - resolvePpathStub = sandbox.stub(projectPath, "resolveProjectPath"); - dirExistsStub = sandbox.stub(fsutils, "dirExistsSync"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should not throw error if functions directory is present", () => { - resolvePpathStub.returns("some/path/to/project"); - dirExistsStub.returns(true); - - expect(() => { - validate.functionsDirectoryExists({ cwd: "cwd" }, "sourceDirName"); - }).to.not.throw(); - }); - - it("should throw error if the functions directory does not exist", () => { - resolvePpathStub.returns("some/path/to/project"); - dirExistsStub.returns(false); - - expect(() => { - validate.functionsDirectoryExists({ cwd: "cwd" }, "sourceDirName"); - }).to.throw(FirebaseError); - }); - }); - - describe("functionNamesAreValid", () => { - it("should allow properly formatted function names", () => { - const functions: any[] = [ - { - id: "my-function-1", - }, - { - id: "my-function-2", - }, - ]; - expect(() => { - validate.functionIdsAreValid(functions); - }).to.not.throw(); - }); - - it("should throw error on improperly formatted function names", () => { - const functions = [ - { - id: "my-function-!@#$%", - platform: "gcfv1", - }, - { - id: "my-function-!@#$!@#", - platform: "gcfv1", - }, - ]; - - expect(() => { - validate.functionIdsAreValid(functions); - }).to.throw(FirebaseError); - }); - - it("should throw error if some function names are improperly formatted", () => { - const functions = [ - { - id: "my-function$%#", - platform: "gcfv1", - }, - { - id: "my-function-2", - platform: "gcfv2", - }, - ]; - - expect(() => { - validate.functionIdsAreValid(functions); - }).to.throw(FirebaseError); - }); - - // I think it should throw error here but it doesn't error on empty or even undefined functionNames. - // TODO(b/131331234): fix this test when validation code path is fixed. - it.skip("should throw error on empty function names", () => { - const functions = [{ id: "", platform: "gcfv1" }]; - - expect(() => { - validate.functionIdsAreValid(functions); - }).to.throw(FirebaseError); - }); - - it("should throw error on capital letters in v2 function names", () => { - const functions = [{ id: "Hi", platform: "gcfv2" }]; - expect(() => { - validate.functionIdsAreValid(functions); - }).to.throw(FirebaseError); - }); - - it("should throw error on underscores in v2 function names", () => { - const functions = [{ id: "o_O", platform: "gcfv2" }]; - expect(() => { - validate.functionIdsAreValid(functions); - }).to.throw(FirebaseError); - }); - }); -}); diff --git a/src/test/deploy/hosting/validate.spec.ts b/src/test/deploy/hosting/validate.spec.ts deleted file mode 100644 index eb6bf4811d2..00000000000 --- a/src/test/deploy/hosting/validate.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { expect } from "chai"; -import { validateDeploy } from "../../../deploy/hosting/validate"; - -const PUBLIC_DIR_ERROR_PREFIX = 'Must supply a "public" directory'; - -describe("validateDeploy()", () => { - function testDeploy(merge: any) { - return () => { - validateDeploy( - { - site: "test-site", - version: "abc123", - config: { ...merge }, - }, - { - cwd: __dirname + "/../../fixtures/simplehosting", - configPath: __dirname + "/../../fixtures/simplehosting/firebase.json", - } - ); - }; - } - - it("should error out if there is no public directory but a 'destination' rewrite", () => { - expect( - testDeploy({ - rewrites: [ - { source: "/foo", destination: "/bar.html" }, - { source: "/baz", function: "app" }, - ], - }) - ).to.throw(PUBLIC_DIR_ERROR_PREFIX); - }); - - it("should error out if there is no public directory and no rewrites or redirects", () => { - expect(testDeploy({})).to.throw(PUBLIC_DIR_ERROR_PREFIX); - }); - - it("should error out if there is no public directory and an i18n with root", () => { - expect( - testDeploy({ - i18n: { root: "/foo" }, - rewrites: [{ source: "/foo", function: "pass" }], - }) - ).to.throw(PUBLIC_DIR_ERROR_PREFIX); - }); - - it("should error out if there is a public directory and an i18n with no root", () => { - expect( - testDeploy({ - public: "public", - i18n: {}, - rewrites: [{ source: "/foo", function: "pass" }], - }) - ).to.throw('Must supply a "root"'); - }); - - it("should pass with public and nothing else", () => { - expect(testDeploy({ public: "public" })).not.to.throw(); - }); - - it("should pass with no public but a function rewrite", () => { - expect(testDeploy({ rewrites: [{ source: "/", function: "app" }] })).not.to.throw(); - }); - - it("should pass with no public but a run rewrite", () => { - expect(testDeploy({ rewrites: [{ source: "/", run: { serviceId: "app" } }] })).not.to.throw(); - }); - - it("should pass with no public but a redirect", () => { - expect( - testDeploy({ redirects: [{ source: "/", destination: "https://google.com/", type: 302 }] }) - ).not.to.throw(); - }); -}); diff --git a/src/test/deploy/remoteconfig/remoteconfig.spec.ts b/src/test/deploy/remoteconfig/remoteconfig.spec.ts deleted file mode 100644 index 75c04a570c9..00000000000 --- a/src/test/deploy/remoteconfig/remoteconfig.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as api from "../../../api"; -import * as rcDeploy from "../../../deploy/remoteconfig/functions"; -import * as remoteconfig from "../../../remoteconfig/get"; -import { RemoteConfigTemplate } from "../../../remoteconfig/interfaces"; - -const PROJECT_NUMBER = "001"; - -const header = { - etag: "etag-344230015214-190", -}; - -function createTemplate(versionNumber: string): RemoteConfigTemplate { - return { - conditions: [ - { - name: "RCTestCondition", - expression: "dateTime < dateTime('2020-07-24T00:00:00', 'America/Los_Angeles')", - }, - ], - parameters: { - RCTestkey: { - defaultValue: { - value: "RCTestValue", - }, - }, - }, - version: { - versionNumber: versionNumber, - updateTime: "2020-07-23T17:13:11.190Z", - updateUser: { - email: "abc@gmail.com", - }, - updateOrigin: "CONSOLE", - updateType: "INCREMENTAL_UPDATE", - }, - parameterGroups: { - RCTestCaseGroup: { - parameters: { - RCTestKey2: { - defaultValue: { - value: "RCTestValue2", - }, - description: "This is a test", - }, - }, - }, - }, - etag: "123", - }; -} - -// Test sample template after deploy -const expectedTemplateInfo: RemoteConfigTemplate = createTemplate("7"); - -// Test sample template before deploy -const currentTemplate: RemoteConfigTemplate = createTemplate("6"); - -describe("Remote Config Deploy", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - let templateStub: sinon.SinonStub; - let etagStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - templateStub = sandbox.stub(remoteconfig, "getTemplate"); - etagStub = sandbox.stub(rcDeploy, "getEtag"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("Publish the updated template", () => { - it("should publish the latest template", async () => { - apiRequestStub.onFirstCall().resolves({ body: expectedTemplateInfo }); - templateStub.withArgs(PROJECT_NUMBER).returns(currentTemplate); - etagStub.withArgs(PROJECT_NUMBER, "6").returns(header); - - const etag = await rcDeploy.getEtag(PROJECT_NUMBER, "6"); - const RCtemplate = await rcDeploy.publishTemplate(PROJECT_NUMBER, currentTemplate, etag); - - expect(RCtemplate).to.deep.equal(expectedTemplateInfo); - expect(apiRequestStub).to.be.calledOnceWith( - "PUT", - `/v1/projects/${PROJECT_NUMBER}/remoteConfig`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - headers: { "If-Match": etag }, - data: { - conditions: currentTemplate.conditions, - parameters: currentTemplate.parameters, - parameterGroups: currentTemplate.parameterGroups, - }, - } - ); - }); - - it("should publish the latest template with * etag", async () => { - apiRequestStub.onFirstCall().resolves({ body: expectedTemplateInfo }); - templateStub.withArgs(PROJECT_NUMBER).returns(currentTemplate); - - const options = { force: true }; - const etag = "*"; - const RCtemplate = await rcDeploy.publishTemplate( - PROJECT_NUMBER, - currentTemplate, - etag, - options - ); - - expect(RCtemplate).to.deep.equal(expectedTemplateInfo); - expect(apiRequestStub).to.be.calledOnceWith( - "PUT", - `/v1/projects/${PROJECT_NUMBER}/remoteConfig`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - headers: { "If-Match": etag }, - data: { - conditions: currentTemplate.conditions, - parameters: currentTemplate.parameters, - parameterGroups: currentTemplate.parameterGroups, - }, - } - ); - }); - - it("should reject if the api call fails", async () => { - const etag = await rcDeploy.getEtag(PROJECT_NUMBER); - - etagStub.withArgs(PROJECT_NUMBER, "undefined").returns(header); - - try { - await rcDeploy.publishTemplate(PROJECT_NUMBER, currentTemplate, etag); - } catch (e) { - e; - } - - expect(apiRequestStub).to.be.calledOnceWith( - "PUT", - `/v1/projects/${PROJECT_NUMBER}/remoteConfig`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - headers: { "If-Match": undefined }, - data: { - conditions: currentTemplate.conditions, - parameters: currentTemplate.parameters, - parameterGroups: currentTemplate.parameterGroups, - }, - } - ); - }); - }); -}); diff --git a/src/test/emulators/auth/deleteAccount.spec.ts b/src/test/emulators/auth/deleteAccount.spec.ts deleted file mode 100644 index 9dd507554f7..00000000000 --- a/src/test/emulators/auth/deleteAccount.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { expect } from "chai"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; -import { - expectStatusCode, - registerUser, - signInWithFakeClaims, - getSigninMethods, - expectUserNotExistsForIdToken, - updateProjectConfig, - deleteAccount, - registerTenant, -} from "./helpers"; - -describeAuthEmulator("accounts:delete", ({ authApi }) => { - it("should delete the user of the idToken", async () => { - const { idToken } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .send({ idToken }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("error"); - }); - - await expectUserNotExistsForIdToken(authApi(), idToken); - }); - - it("should error when trying to delete by localId without OAuth", async () => { - const { localId } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .send({ localId }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_ID_TOKEN"); - }); - }); - - it("should remove federated accounts for user", async () => { - const email = "alice@example.com"; - const providerId = "google.com"; - const sub = "12345"; - const { localId, idToken } = await signInWithFakeClaims(authApi(), providerId, { - sub, - email, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .query({ key: "fake-api-key" }) - .send({ idToken }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("error"); - }); - - expect(await getSigninMethods(authApi(), email)).to.be.empty; - - const signInAgain = await signInWithFakeClaims(authApi(), providerId, { - sub, - email, - }); - expect(signInAgain.localId).not.to.equal(localId); - }); - - it("should delete the user by localId if OAuth credentials are present", async () => { - const { localId, idToken } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .set("Authorization", "Bearer owner") - .send({ localId }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("error"); - }); - - await expectUserNotExistsForIdToken(authApi(), idToken); - }); - - it("should error if missing localId when OAuth credentials are present", async () => { - const { idToken } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .set("Authorization", "Bearer owner") - .send({ idToken /* no localId */ }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_LOCAL_ID"); - }); - }); - - it("should error on delete with idToken if usageMode is passthrough", async () => { - const { idToken } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .send({ idToken }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - - it("should return not found on delete with localId if usageMode is passthrough", async () => { - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .set("Authorization", "Bearer owner") - .send({ localId: "does-not-exist" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("USER_NOT_FOUND"); - }); - }); - - it("should delete the user of the idToken", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:delete") - .send({ tenantId: tenant.tenantId }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").includes("PROJECT_DISABLED"); - }); - }); -}); diff --git a/src/test/emulators/auth/emailLink.spec.ts b/src/test/emulators/auth/emailLink.spec.ts deleted file mode 100644 index 3636500ed3a..00000000000 --- a/src/test/emulators/auth/emailLink.spec.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; -import { - expectStatusCode, - registerUser, - signInWithPhoneNumber, - updateAccountByLocalId, - getSigninMethods, - inspectOobs, - createEmailSignInOob, - TEST_PHONE_NUMBER, - TEST_MFA_INFO, - updateProjectConfig, - registerTenant, - getAccountInfoByLocalId, -} from "./helpers"; - -describeAuthEmulator("email link sign-in", ({ authApi }) => { - it("should send OOB code to new emails and create account on sign-in", async () => { - const email = "alice@example.com"; - await createEmailSignInOob(authApi(), email); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(1); - expect(oobs[0].email).to.equal(email); - expect(oobs[0].requestType).to.equal("EMAIL_SIGNIN"); - - // The returned oobCode can be redeemed to sign-in. - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ oobCode: oobs[0].oobCode, email }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("idToken").that.is.a("string"); - expect(res.body.email).to.equal(email); - expect(res.body.isNewUser).to.equal(true); - - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - expect(decoded!.payload.user_id).to.be.a("string"); - expect(decoded!.payload).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); // The provider name is (confusingly) "password". - }); - - expect(await getSigninMethods(authApi(), email)).to.have.members(["emailLink"]); - }); - - it("should sign an existing account in and enable email-link sign-in for them", async () => { - const user = { email: "bob@example.com", password: "notasecret" }; - const { localId, idToken } = await registerUser(authApi(), user); - const { oobCode } = await createEmailSignInOob(authApi(), user.email); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email: user.email, oobCode }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") - .query({ key: "fake-api-key" }) - .send({ idToken }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.users).to.have.length(1); - expect(res.body.users[0]).to.have.property("emailLinkSignin").equal(true); - }); - - expect(await getSigninMethods(authApi(), user.email)).to.have.members([ - "password", - "emailLink", - ]); - }); - - it("should error on signInWithEmailLink if usageMode is passthrough", async () => { - const user = { email: "bob@example.com", password: "notasecret" }; - const { oobCode } = await createEmailSignInOob(authApi(), user.email); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email: user.email, oobCode }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - - it("should error on invalid oobCode", async () => { - const email = "alice@example.com"; - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode: "invalid" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("INVALID_OOB_CODE"); - }); - }); - - it("should error if user is disabled", async () => { - const { localId, email } = await registerUser(authApi(), { - email: "bob@example.com", - password: "notasecret", - }); - const { oobCode } = await createEmailSignInOob(authApi(), email); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("USER_DISABLED"); - }); - }); - - it("should error if email mismatches", async () => { - const { oobCode } = await createEmailSignInOob(authApi(), "alice@example.com"); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email: "NOT-alice@example.com", oobCode }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal( - "INVALID_EMAIL : The email provided does not match the sign-in email address." - ); - }); - }); - - it("should link existing account with idToken to new email", async () => { - const oldEmail = "bob@example.com"; - const newEmail = "alice@example.com"; - const { localId, idToken } = await registerUser(authApi(), { - email: oldEmail, - password: "notasecret", - }); - const { oobCode } = await createEmailSignInOob(authApi(), newEmail); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email: newEmail, oobCode, idToken }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - expect(res.body.email).to.equal(newEmail); - }); - - expect(await getSigninMethods(authApi(), newEmail)).to.have.members(["password", "emailLink"]); - expect(await getSigninMethods(authApi(), oldEmail)).to.be.empty; - }); - - it("should link existing phone-auth account to new email", async () => { - const { localId, idToken } = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); - const email = "alice@example.com"; - const { oobCode } = await createEmailSignInOob(authApi(), email); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode, idToken }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - expect(res.body.email).to.equal(email); - }); - - // Sign-in methods should not contain "phone", since phone sign-in is not - // associated with an email address. - expect(await getSigninMethods(authApi(), email)).to.have.members(["emailLink"]); - }); - - it("should error when trying to link an email already used in another account", async () => { - const { idToken } = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); - const email = "alice@example.com"; - await registerUser(authApi(), { email, password: "notasecret" }); - const { oobCode } = await createEmailSignInOob(authApi(), email); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode, idToken }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); - }); - }); - - it("should error if user to be linked is disabled", async () => { - const { email, localId, idToken } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - const { oobCode } = await createEmailSignInOob(authApi(), email); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode, idToken }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); - }); - }); - - it("should return pending credential if user has MFA", async () => { - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO], - }; - const { idToken, email } = await registerUser(authApi(), user); - const { oobCode } = await createEmailSignInOob(authApi(), email); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode, idToken }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("idToken"); - expect(res.body).not.to.have.property("refreshToken"); - expect(res.body.mfaPendingCredential).to.be.a("string"); - expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); - }); - }); - - it("should error if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.include("PROJECT_DISABLED"); - }); - }); - - it("should error if email link sign in is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - enableEmailLinkSignin: false, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.include("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should create account on sign-in with tenantId", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - enableEmailLinkSignin: true, - }); - const email = "alice@example.com"; - const { oobCode } = await createEmailSignInOob(authApi(), email, tenant.tenantId); - - const localId = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ oobCode, email, tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(200, res); - return res.body.localId; - }); - - const user = await getAccountInfoByLocalId(authApi(), localId, tenant.tenantId); - expect(user.tenantId).to.eql(tenant.tenantId); - }); - - it("should return pending credential if user has MFA and enabled on tenant projects", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - enableEmailLinkSignin: true, - allowPasswordSignup: true, - mfaConfig: { - state: "ENABLED", - }, - }); - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO], - tenantId: tenant.tenantId, - }; - const { idToken, email } = await registerUser(authApi(), user); - const { oobCode } = await createEmailSignInOob(authApi(), email, tenant.tenantId); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode, idToken, tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("idToken"); - expect(res.body).not.to.have.property("refreshToken"); - expect(res.body.mfaPendingCredential).to.be.a("string"); - expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); - }); - }); -}); diff --git a/src/test/emulators/auth/mfa.spec.ts b/src/test/emulators/auth/mfa.spec.ts deleted file mode 100644 index 49bb11be688..00000000000 --- a/src/test/emulators/auth/mfa.spec.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { expect } from "chai"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { - enrollPhoneMfa, - expectStatusCode, - getAccountInfoByIdToken, - getAccountInfoByLocalId, - inspectVerificationCodes, - registerTenant, - registerUser, - signInWithEmailLink, - signInWithPhoneNumber, - TEST_PHONE_NUMBER, - TEST_PHONE_NUMBER_2, - TEST_PHONE_NUMBER_OBFUSCATED, - updateAccountByLocalId, -} from "./helpers"; -import { MfaEnrollment } from "../../../emulator/auth/types"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; - -describeAuthEmulator("mfa enrollment", ({ authApi, getClock }) => { - it("should error if account does not have email verified", async () => { - const { idToken } = await registerUser(authApi(), { - email: "unverified@example.com", - password: "testing", - }); - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ idToken, phoneEnrollmentInfo: { phoneNumber: TEST_PHONE_NUMBER } }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal( - "UNVERIFIED_EMAIL : Need to verify email first before enrolling second factors." - ); - }); - }); - - it("should allow phone enrollment for an existing account", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ idToken, phoneEnrollmentInfo: { phoneNumber } }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.phoneSessionInfo.sessionInfo).to.be.a("string"); - return res.body.phoneSessionInfo.sessionInfo as string; - }); - - const codes = await inspectVerificationCodes(authApi()); - expect(codes).to.have.length(1); - expect(codes[0].phoneNumber).to.equal(phoneNumber); - expect(codes[0].sessionInfo).to.equal(sessionInfo); - expect(codes[0].code).to.be.a("string"); - const { code } = codes[0]; - - const res = await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") - .query({ key: "fake-api-key" }) - .send({ idToken, phoneVerificationInfo: { code, sessionInfo } }); - - expectStatusCode(200, res); - expect(res.body.idToken).to.be.a("string"); - expect(res.body.refreshToken).to.be.a("string"); - - const userInfo = await getAccountInfoByIdToken(authApi(), idToken); - expect(userInfo.mfaInfo).to.be.an("array").with.lengthOf(1); - expect(userInfo.mfaInfo![0].phoneInfo).to.equal(phoneNumber); - const mfaEnrollmentId = userInfo.mfaInfo![0].mfaEnrollmentId; - - const decoded = decodeJwt(res.body.idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.payload.firebase.sign_in_second_factor).to.equal("phone"); - expect(decoded!.payload.firebase.second_factor_identifier).to.equal(mfaEnrollmentId); - }); - - it("should error if phoneEnrollmentInfo is not specified", async () => { - const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ idToken }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.contain("INVALID_ARGUMENT"); - }); - }); - - it("should error if phoneNumber is invalid", async () => { - const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ idToken, phoneEnrollmentInfo: { phoneNumber: "notaphonenumber" } }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.contain("INVALID_PHONE_NUMBER"); - }); - }); - - it("should error if phoneNumber is a duplicate", async () => { - const { idToken } = await signInWithEmailLink(authApi(), "foo@example.com"); - await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ idToken, phoneEnrollmentInfo: { phoneNumber: TEST_PHONE_NUMBER } }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal( - "SECOND_FACTOR_EXISTS : Phone number already enrolled as second factor for this account." - ); - }); - }); - - it("should error if sign-in method of idToken is ineligible for MFA", async () => { - const { idToken, localId } = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); - await updateAccountByLocalId(authApi(), localId, { - email: "bob@example.com", - emailVerified: true, - }); - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ idToken, phoneEnrollmentInfo: { phoneNumber: TEST_PHONE_NUMBER_2 } }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal( - "UNSUPPORTED_FIRST_FACTOR : MFA is not available for the given first factor." - ); - }); - }); - - it("should error on mfaEnrollment:start if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); - - it("should error on mfaEnrollment:start if MFA is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "DISABLED", - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should error on mfaEnrollment:start if phone SMS is not an enabled provider", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "ENABLED", - enabledProviders: ["PROVIDER_UNSPECIFIED"], - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should error on mfaEnrollment:finalize if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); - - it("should error on mfaEnrollment:finalize if MFA is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "DISABLED", - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should error on mfaEnrollment:finalize if phone SMS is not an enabled provider", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "ENABLED", - enabledProviders: ["PROVIDER_UNSPECIFIED"], - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should allow sign-in with pending credential for MFA-enabled user", async () => { - const email = "foo@example.com"; - const password = "abcdef"; - const { idToken, localId } = await registerUser(authApi(), { email, password }); - await updateAccountByLocalId(authApi(), localId, { emailVerified: true }); - await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); - const beforeSignIn = await getAccountInfoByLocalId(authApi(), localId); - - getClock().tick(3333); - - const { mfaPendingCredential, mfaEnrollmentId } = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email, password }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("idToken"); - expect(res.body).not.to.have.property("refreshToken"); - const mfaPendingCredential = res.body.mfaPendingCredential as string; - const mfaInfo = res.body.mfaInfo as MfaEnrollment[]; - expect(mfaPendingCredential).to.be.a("string"); - expect(mfaInfo).to.be.an("array").with.lengthOf(1); - expect(mfaInfo[0]?.phoneInfo).to.equal(TEST_PHONE_NUMBER_OBFUSCATED); - - // This must not be exposed right after first factor login. - expect(mfaInfo[0]?.phoneInfo).not.to.have.property("unobfuscatedPhoneInfo"); - return { mfaPendingCredential, mfaEnrollmentId: mfaInfo[0].mfaEnrollmentId }; - }); - - // Login / refresh timestamps should not change until MFA was successful. - const afterFirstFactor = await getAccountInfoByLocalId(authApi(), localId); - expect(afterFirstFactor.lastLoginAt).to.equal(beforeSignIn.lastLoginAt); - expect(afterFirstFactor.lastRefreshAt).to.equal(beforeSignIn.lastRefreshAt); - - getClock().tick(4444); - - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") - .query({ key: "fake-api-key" }) - .send({ - mfaEnrollmentId, - mfaPendingCredential, - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.phoneResponseInfo.sessionInfo).to.be.a("string"); - return res.body.phoneResponseInfo.sessionInfo as string; - }); - - const code = (await inspectVerificationCodes(authApi()))[0].code; - - getClock().tick(5555); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") - .query({ key: "fake-api-key" }) - .send({ - mfaPendingCredential, - phoneVerificationInfo: { - sessionInfo, - code: code, - }, - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.idToken).to.be.a("string"); - expect(res.body.refreshToken).to.be.a("string"); - - const decoded = decodeJwt(res.body.idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.payload.firebase.sign_in_second_factor).to.equal("phone"); - expect(decoded!.payload.firebase.second_factor_identifier).to.equal(mfaEnrollmentId); - }); - - // Login / refresh timestamps should now be updated. - const afterMfa = await getAccountInfoByLocalId(authApi(), localId); - expect(afterMfa.lastLoginAt).to.equal(Date.now().toString()); - expect(afterMfa.lastRefreshAt).to.equal(new Date().toISOString()); - }); - - it("should error on mfaSignIn:start if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); - - it("should error on mfaSignIn:start if MFA is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "DISABLED", - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should error on mfaSignIn:start if phone SMS is not an enabled provider", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "ENABLED", - enabledProviders: ["PROVIDER_UNSPECIFIED"], - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:start") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should error on mfaSignIn:finalize if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); - - it("should error on mfaSignIn:finalize if MFA is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "DISABLED", - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should error on mfaSignIn:finalize if phone SMS is not an enabled provider", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - mfaConfig: { - state: "ENABLED", - enabledProviders: ["PROVIDER_UNSPECIFIED"], - }, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaSignIn:finalize") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contains("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should allow withdrawing MFA for a user", async () => { - const { idToken: token1 } = await signInWithEmailLink(authApi(), "foo@example.com"); - const { idToken } = await enrollPhoneMfa(authApi(), token1, TEST_PHONE_NUMBER); - - const { mfaInfo } = await getAccountInfoByIdToken(authApi(), idToken); - expect(mfaInfo).to.have.lengthOf(1); - const { mfaEnrollmentId } = mfaInfo![0]!; - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:withdraw") - .query({ key: "fake-api-key" }) - .send({ idToken, mfaEnrollmentId }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.idToken).to.be.a("string"); - expect(res.body.refreshToken).to.be.a("string"); - - const decoded = decodeJwt(res.body.idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.payload.firebase).not.to.have.property("sign_in_second_factor"); - expect(decoded!.payload.firebase).not.to.have.property("second_factor_identifier"); - }); - - const after = await getAccountInfoByIdToken(authApi(), idToken); - expect(after.mfaInfo).to.have.lengthOf(0); - }); - - it("should error on mfaEnrollment:withdraw if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:withdraw") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); -}); diff --git a/src/test/emulators/auth/misc.spec.ts b/src/test/emulators/auth/misc.spec.ts deleted file mode 100644 index c0d69dd3f9d..00000000000 --- a/src/test/emulators/auth/misc.spec.ts +++ /dev/null @@ -1,555 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { UserInfo } from "../../../emulator/auth/state"; -import { - deleteAccount, - getAccountInfoByIdToken, - PROJECT_ID, - registerTenant, - signInWithPhoneNumber, - TEST_PHONE_NUMBER, - updateProjectConfig, -} from "./helpers"; -import { describeAuthEmulator } from "./setup"; -import { - expectStatusCode, - registerUser, - registerAnonUser, - updateAccountByLocalId, - expectUserNotExistsForIdToken, -} from "./helpers"; -import { - FirebaseJwtPayload, - SESSION_COOKIE_MAX_VALID_DURATION, -} from "../../../emulator/auth/operations"; -import { toUnixTimestamp } from "../../../emulator/auth/utils"; - -describeAuthEmulator("token refresh", ({ authApi, getClock }) => { - it("should exchange refresh token for new tokens", async () => { - const { refreshToken, localId } = await registerAnonUser(authApi()); - await authApi() - .post("/securetoken.googleapis.com/v1/token") - .type("form") - // snake_case parameters also work, per OAuth 2.0 spec. - .send({ refresh_token: refreshToken, grantType: "refresh_token" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.id_token).to.be.a("string"); - expect(res.body.access_token).to.equal(res.body.id_token); - expect(res.body.refresh_token).to.be.a("string"); - expect(res.body.expires_in) - .to.be.a("string") - .matches(/[0-9]+/); - expect(res.body.project_id).to.equal("12345"); - expect(res.body.token_type).to.equal("Bearer"); - expect(res.body.user_id).to.equal(localId); - }); - }); - - it("should populate auth_time to match lastLoginAt (in seconds since epoch)", async () => { - getClock().tick(444); // Make timestamps a bit more interesting (non-zero). - const emailUser = { email: "alice@example.com", password: "notasecret" }; - const { refreshToken } = await registerUser(authApi(), emailUser); - - getClock().tick(2000); // Wait 2 seconds before refreshing. - - const res = await authApi() - .post("/securetoken.googleapis.com/v1/token") - .type("form") - // snake_case parameters also work, per OAuth 2.0 spec. - .send({ refresh_token: refreshToken, grantType: "refresh_token" }) - .query({ key: "fake-api-key" }); - - const idToken = res.body.id_token; - const user = await getAccountInfoByIdToken(authApi(), idToken); - expect(user.lastLoginAt).not.to.be.undefined; - const lastLoginAtSeconds = Math.floor(parseInt(user.lastLoginAt!, 10) / 1000); - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - // This should match login time, not token refresh time. - expect(decoded!.payload.auth_time).to.equal(lastLoginAtSeconds); - }); - - it("should error if user is disabled", async () => { - const { refreshToken, localId } = await registerAnonUser(authApi()); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - await authApi() - .post("/securetoken.googleapis.com/v1/token") - .type("form") - .send({ refreshToken: refreshToken, grantType: "refresh_token" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("USER_DISABLED"); - }); - }); - - it("should error if usageMode is passthrough", async () => { - const { refreshToken, idToken } = await registerAnonUser(authApi()); - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/securetoken.googleapis.com/v1/token") - .type("form") - .send({ refresh_token: refreshToken, grantType: "refresh_token" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); -}); - -describeAuthEmulator("createSessionCookie", ({ authApi }) => { - it("should return a valid sessionCookie", async () => { - const { idToken } = await registerAnonUser(authApi()); - const validDuration = 7777; /* seconds */ - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) - .set("Authorization", "Bearer owner") - .send({ idToken, validDuration: validDuration.toString() }) - .then((res) => { - expectStatusCode(200, res); - const sessionCookie = res.body.sessionCookie; - expect(sessionCookie).to.be.a("string"); - - const decoded = decodeJwt(sessionCookie, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "session cookie is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - expect(decoded!.payload.iat).to.equal(toUnixTimestamp(new Date())); - expect(decoded!.payload.exp).to.equal(toUnixTimestamp(new Date()) + validDuration); - expect(decoded!.payload.iss).to.equal(`https://session.firebase.google.com/${PROJECT_ID}`); - - const idTokenProps = decodeJwt(idToken) as Partial; - delete idTokenProps.iss; - delete idTokenProps.iat; - delete idTokenProps.exp; - expect(decoded!.payload).to.deep.contain(idTokenProps); - }); - }); - - it("should throw if idToken is missing", async () => { - const validDuration = 7777; /* seconds */ - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) - .set("Authorization", "Bearer owner") - .send({ validDuration: validDuration.toString() /* no idToken */ }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("MISSING_ID_TOKEN"); - }); - }); - - it("should throw if idToken is invalid", async () => { - const validDuration = 7777; /* seconds */ - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) - .set("Authorization", "Bearer owner") - .send({ idToken: "invalid", validDuration: validDuration.toString() }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("INVALID_ID_TOKEN"); - }); - }); - - it("should use default session cookie validDuration if not specified", async () => { - const { idToken } = await registerAnonUser(authApi()); - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) - .set("Authorization", "Bearer owner") - .send({ idToken }) - .then((res) => { - expectStatusCode(200, res); - const sessionCookie = res.body.sessionCookie; - expect(sessionCookie).to.be.a("string"); - - const decoded = decodeJwt(sessionCookie, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "session cookie is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - expect(decoded!.payload.exp).to.equal( - toUnixTimestamp(new Date()) + SESSION_COOKIE_MAX_VALID_DURATION - ); - }); - }); - - it("should throw if validDuration is too short or too long", async () => { - const { idToken } = await registerAnonUser(authApi()); - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) - .set("Authorization", "Bearer owner") - .send({ idToken, validDuration: "1" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("INVALID_DURATION"); - }); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) - .set("Authorization", "Bearer owner") - .send({ idToken, validDuration: "999999999999" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("INVALID_DURATION"); - }); - }); - - it("should error if usageMode is passthrough", async () => { - const { idToken } = await registerAnonUser(authApi()); - const validDuration = 7777; /* seconds */ - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}:createSessionCookie`) - .set("Authorization", "Bearer owner") - .send({ idToken, validDuration: validDuration.toString() }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); -}); - -describeAuthEmulator("accounts:lookup", ({ authApi }) => { - it("should return user by localId when privileged", async () => { - const { localId } = await registerAnonUser(authApi()); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) - .set("Authorization", "Bearer owner") - .send({ localId: [localId] }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.users).to.have.length(1); - expect(res.body.users[0].localId).to.equal(localId); - }); - }); - - it("should deduplicate users", async () => { - const { localId } = await registerAnonUser(authApi()); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) - .set("Authorization", "Bearer owner") - .send({ localId: [localId, localId] /* two with the same id */ }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.users).to.have.length(1); - expect(res.body.users[0].localId).to.equal(localId); - }); - }); - - it("should return providerUserInfo for phone auth users", async () => { - const { localId } = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) - .set("Authorization", "Bearer owner") - .send({ localId: [localId] }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.users).to.have.length(1); - expect(res.body.users[0].providerUserInfo).to.eql([ - { - phoneNumber: TEST_PHONE_NUMBER, - rawId: TEST_PHONE_NUMBER, - providerId: "phone", - }, - ]); - }); - }); - - it("should return empty result when localId is not found", async () => { - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) - .set("Authorization", "Bearer owner") - .send({ localId: ["noSuchId"] }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("users"); - }); - }); - - it("should return empty result for admin lookup if usageMode is passthrough", async () => { - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) - .set("Authorization", "Bearer owner") - .send({ localId: ["noSuchId"] }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("users"); - }); - }); - - it("should return user by tenantId in idToken", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - allowPasswordSignup: true, - }); - const { idToken, localId } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - tenantId: tenant.tenantId, - }); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/accounts:lookup`) - .send({ idToken }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.users).to.have.length(1); - expect(res.body.users[0].localId).to.equal(localId); - }); - }); - - it("should error for lookup using idToken if usageMode is passthrough", async () => { - const { idToken } = await registerAnonUser(authApi()); - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/accounts:lookup`) - .send({ idToken }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - - it("should error if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") - .set("Authorization", "Bearer owner") - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").includes("PROJECT_DISABLED"); - }); - }); -}); - -describeAuthEmulator("accounts:query", ({ authApi }) => { - it("should return count of accounts when returnUserInfo is false", async () => { - await registerAnonUser(authApi()); - await registerAnonUser(authApi()); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:query`) - .set("Authorization", "Bearer owner") - .send({ returnUserInfo: false }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.recordsCount).to.equal("2"); // string (int64 format) - expect(res.body).not.to.have.property("userInfo"); - }); - }); - - it("should return accounts when returnUserInfo is true", async () => { - const { localId } = await registerAnonUser(authApi()); - const user = { email: "alice@example.com", password: "notasecret" }; - const { localId: localId2 } = await registerUser(authApi(), user); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:query`) - .set("Authorization", "Bearer owner") - .send({ - /* returnUserInfo is true by default */ - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.recordsCount).to.equal("2"); // string (int64 format) - expect(res.body.userInfo).to.be.an.instanceof(Array).with.lengthOf(2); - - const users = res.body.userInfo as UserInfo[]; - expect(users[0].localId < users[1].localId, "users are not sorted by ID ASC").to.be.true; - const anonUser = users.find((x) => x.localId === localId); - expect(anonUser, "cannot find first registered user").to.be.not.undefined; - - const emailUser = users.find((x) => x.localId === localId2); - expect(emailUser, "cannot find second registered user").to.be.not.undefined; - expect(emailUser!.email).to.equal(user.email); - }); - }); - - it("should error if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:query`) - .set("Authorization", "Bearer owner") - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); -}); - -describeAuthEmulator("emulator utility APIs", ({ authApi }) => { - it("should drop all accounts on DELETE /emulator/v1/projects/{PROJECT_ID}/accounts", async () => { - const user1 = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - const user2 = await registerUser(authApi(), { - email: "bob@example.com", - password: "notasecret2", - }); - await authApi() - .delete(`/emulator/v1/projects/${PROJECT_ID}/accounts`) - .send() - .then((res) => expectStatusCode(200, res)); - - await expectUserNotExistsForIdToken(authApi(), user1.idToken); - await expectUserNotExistsForIdToken(authApi(), user2.idToken); - }); - - it("should drop all accounts on DELETE /emulator/v1/projects/{PROJECT_ID}/tenants/{TENANT_ID}/accounts", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - allowPasswordSignup: true, - }); - const user1 = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - tenantId: tenant.tenantId, - }); - const user2 = await registerUser(authApi(), { - email: "bob@example.com", - password: "notasecret2", - tenantId: tenant.tenantId, - }); - - await authApi() - .delete(`/emulator/v1/projects/${PROJECT_ID}/tenants/${tenant.tenantId}/accounts`) - .send() - .then((res) => expectStatusCode(200, res)); - - await expectUserNotExistsForIdToken(authApi(), user1.idToken, tenant.tenantId); - await expectUserNotExistsForIdToken(authApi(), user2.idToken, tenant.tenantId); - }); - - it("should return config on GET /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await authApi() - .get(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send() - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("signIn").eql({ - allowDuplicateEmails: false /* default value */, - }); - }); - }); - - it("should update allowDuplicateEmails on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ signIn: { allowDuplicateEmails: true } }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("signIn").eql({ - allowDuplicateEmails: true, - }); - }); - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ signIn: { allowDuplicateEmails: false } }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("signIn").eql({ - allowDuplicateEmails: false, - }); - }); - }); - - it("should update usageMode on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ usageMode: "PASSTHROUGH" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("usageMode").equals("PASSTHROUGH"); - }); - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ usageMode: "DEFAULT" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("usageMode").equals("DEFAULT"); - }); - }); - - it("should default to DEFAULT usageMode on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ usageMode: undefined }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("usageMode").equals("DEFAULT"); - }); - }); - - it("should error for unspecified usageMode on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ usageMode: "USAGE_MODE_UNSPECIFIED" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("Invalid usage mode provided"); - }); - }); - - it("should error for invalid usageMode on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ usageMode: "NOT_AN_ACTUAL_USAGE_MODE" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .contains("Invalid JSON payload received"); - }); - }); - - it("should error when users are present for passthrough mode on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await registerAnonUser(authApi()); - - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ usageMode: "PASSTHROUGH" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("Users are present, unable to set passthrough mode"); - }); - }); -}); diff --git a/src/test/emulators/auth/password.spec.ts b/src/test/emulators/auth/password.spec.ts deleted file mode 100644 index 9fed55d1aa6..00000000000 --- a/src/test/emulators/auth/password.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; -import { - deleteAccount, - expectStatusCode, - getAccountInfoByLocalId, - registerTenant, - registerUser, - TEST_MFA_INFO, - updateAccountByLocalId, - updateProjectConfig, -} from "./helpers"; - -describeAuthEmulator("accounts:signInWithPassword", ({ authApi, getClock }) => { - it("should issue tokens when email and password are valid", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { localId } = await registerUser(authApi(), user); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: user.email, password: user.password }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).equals(localId); - expect(res.body.email).equals(user.email); - expect(res.body).to.have.property("registered").equals(true); - expect(res.body).to.have.property("refreshToken").that.is.a("string"); - - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - expect(decoded!.payload.user_id).to.equal(localId); - expect(decoded!.payload).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); - }); - }); - - it("should update lastLoginAt on successful login", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { localId } = await registerUser(authApi(), user); - - const beforeLogin = await getAccountInfoByLocalId(authApi(), localId); - expect(beforeLogin.lastLoginAt).to.equal(Date.now().toString()); - - getClock().tick(4000); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: user.email, password: user.password }) - .then((res) => { - expectStatusCode(200, res); - }); - - const afterLogin = await getAccountInfoByLocalId(authApi(), localId); - expect(afterLogin.lastLoginAt).to.equal(Date.now().toString()); - }); - - it("should validate email address ignoring case", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { localId } = await registerUser(authApi(), user); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: "AlIcE@exAMPle.COM", password: user.password }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).equals(localId); - }); - }); - - it("should error if email or password is missing", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ /* no email */ password: "notasecret" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).equals("MISSING_EMAIL"); - }); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: "nosuchuser@example.com" /* no password */ }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).equals("MISSING_PASSWORD"); - }); - }); - - it("should error if email is not found", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: "nosuchuser@example.com", password: "notasecret" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).equals("EMAIL_NOT_FOUND"); - }); - }); - - it("should error if password is wrong", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - await registerUser(authApi(), user); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - // Passwords are case sensitive. The uppercase one below doesn't match. - .send({ email: user.email, password: "NOTASECRET" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).equals("INVALID_PASSWORD"); - }); - }); - - it("should error if user is disabled", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { localId } = await registerUser(authApi(), user); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: user.email, password: "notasecret" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("USER_DISABLED"); - }); - }); - - it("should return pending credential if user has MFA", async () => { - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO], - }; - await registerUser(authApi(), user); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: user.email, password: user.password }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("idToken"); - expect(res.body).not.to.have.property("refreshToken"); - expect(res.body.mfaPendingCredential).to.be.a("string"); - expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); - }); - }); - - it("should error if usageMode is passthrough", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { localId, idToken } = await registerUser(authApi(), user); - await deleteAccount(authApi(), { idToken }); - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ email: user.email, password: user.password }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - - it("should error if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.include("PROJECT_DISABLED"); - }); - }); - - it("should error if password sign up is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - allowPasswordSignup: false, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.include("PASSWORD_LOGIN_DISABLED"); - }); - }); - - it("should return pending credential if user has MFA and enabled on tenant projects", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { - disableAuth: false, - allowPasswordSignup: true, - mfaConfig: { - state: "ENABLED", - }, - }); - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO], - tenantId: tenant.tenantId, - }; - await registerUser(authApi(), user); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId, email: user.email, password: user.password }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("idToken"); - expect(res.body).not.to.have.property("refreshToken"); - expect(res.body.mfaPendingCredential).to.be.a("string"); - expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); - }); - }); -}); diff --git a/src/test/emulators/auth/phone.spec.ts b/src/test/emulators/auth/phone.spec.ts deleted file mode 100644 index 561a6583cc9..00000000000 --- a/src/test/emulators/auth/phone.spec.ts +++ /dev/null @@ -1,458 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; -import { - expectStatusCode, - registerAnonUser, - signInWithPhoneNumber, - updateAccountByLocalId, - inspectVerificationCodes, - registerUser, - TEST_MFA_INFO, - TEST_PHONE_NUMBER, - TEST_PHONE_NUMBER_2, - enrollPhoneMfa, - updateProjectConfig, - registerTenant, -} from "./helpers"; - -describeAuthEmulator("phone auth sign-in", ({ authApi }) => { - it("should return fake recaptcha params", async () => { - await authApi() - .get("/identitytoolkit.googleapis.com/v1/recaptchaParams") - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("recaptchaStoken").that.is.a("string"); - expect(res.body).to.have.property("recaptchaSiteKey").that.is.a("string"); - }); - }); - - it("should pretend to send a verification code via SMS", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("sessionInfo").that.is.a("string"); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - expect(codes).to.have.length(1); - expect(codes[0].phoneNumber).to.equal(phoneNumber); - expect(codes[0].sessionInfo).to.equal(sessionInfo); - expect(codes[0].code).to.be.a("string"); - }); - - it("should error when phone number is missing when calling sendVerificationCode", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ recaptchaToken: "ignored" /* no phone number */ }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - // This matches the production behavior. For some reason, it's not MISSING_PHONE_NUMBER. - .equals("INVALID_PHONE_NUMBER : Invalid format."); - }); - }); - - it("should error when phone number is invalid", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ recaptchaToken: "ignored", phoneNumber: "invalid" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("INVALID_PHONE_NUMBER : Invalid format."); - }); - }); - - it("should error on sendVerificationMode if usageMode is passthrough", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - - it("should error on sendVerificationCode if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); - - it("should error on sendVerificationCode for tenant projects", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: false }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("UNSUPPORTED_TENANT_OPERATION"); - }); - }); - - it("should create new account by verifying phone number", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("isNewUser").equals(true); - expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); - - expect(res.body).to.have.property("refreshToken").that.is.a("string"); - - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - expect(decoded!.payload.user_id).to.be.a("string"); - expect(decoded!.payload.phone_number).to.equal(phoneNumber); - expect(decoded!.payload).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("phone"); - expect(decoded!.payload.firebase.identities).to.eql({ phone: [phoneNumber] }); - }); - }); - - it("should error when sessionInfo or code is missing for signInWithPhoneNumber", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ code: "123456" /* no sessionInfo */ }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_SESSION_INFO"); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo: "something-something" /* no code */ }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_CODE"); - }); - }); - - it("should error when sessionInfo or code is invalid", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo: "totally-invalid", code }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("INVALID_SESSION_INFO"); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - // Try to send the code but with an extra "1" appended. - // This is definitely invalid since we won't have another pending code. - .send({ sessionInfo, code: code + "1" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("INVALID_CODE"); - }); - }); - - it("should error if user is disabled", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - const { localId } = await signInWithPhoneNumber(authApi(), phoneNumber); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); - }); - }); - - it("should link phone number to existing account by idToken", async () => { - const { localId, idToken } = await registerAnonUser(authApi()); - - const phoneNumber = TEST_PHONE_NUMBER; - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code, idToken }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("isNewUser").equals(false); - expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); - expect(res.body.localId).to.equal(localId); - }); - }); - - it("should error if user to be linked is disabled", async () => { - const { localId, idToken } = await registerAnonUser(authApi()); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - const phoneNumber = TEST_PHONE_NUMBER; - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code, idToken }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); - }); - }); - - it("should error when linking phone number to existing user with MFA", async () => { - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO], - }; - const { idToken } = await registerUser(authApi(), user); - - const phoneNumber = TEST_PHONE_NUMBER; - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo as string; - }); - - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code, idToken }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal( - "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user." - ); - }); - }); - - it("should error if user has MFA", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - let { idToken, localId } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - await updateAccountByLocalId(authApi(), localId, { - emailVerified: true, - phoneNumber, - }); - ({ idToken } = await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER_2)); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal( - "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user." - ); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - expect(codes).to.be.empty; - }); - - it("should return temporaryProof if phone number already belongs to another account", async () => { - // Given a phone number that is already registered... - const phoneNumber = TEST_PHONE_NUMBER; - await signInWithPhoneNumber(authApi(), phoneNumber); - - const { idToken } = await registerAnonUser(authApi()); - - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo; - }); - - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - - const temporaryProof = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code, idToken }) - .then((res) => { - expectStatusCode(200, res); - // The linking will fail, but a successful response is still returned - // with a temporaryProof (so that clients may call this API again - // without having to verify the phone number again). - expect(res.body).not.to.have.property("idToken"); - expect(res.body).to.have.property("phoneNumber").equals(phoneNumber); - expect(res.body.temporaryProof).to.be.a("string"); - return res.body.temporaryProof; - }); - - // When called again with the returned temporaryProof, the real error - // message should now be returned. - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ idToken, phoneNumber, temporaryProof }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PHONE_NUMBER_EXISTS"); - }); - }); - - it("should error on signInWithPhoneNumber if usageMode is passthrough", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - const sessionInfo = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") - .query({ key: "fake-api-key" }) - .send({ phoneNumber, recaptchaToken: "ignored" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.sessionInfo; - }); - const codes = await inspectVerificationCodes(authApi()); - const code = codes[0].code; - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - - it("should error if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("PROJECT_DISABLED"); - }); - }); - - it("should error if called on tenant project", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: false }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("UNSUPPORTED_TENANT_OPERATION"); - }); - }); -}); diff --git a/src/test/emulators/auth/setup.ts b/src/test/emulators/auth/setup.ts deleted file mode 100644 index b9ac28b1758..00000000000 --- a/src/test/emulators/auth/setup.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Suite } from "mocha"; -import { useFakeTimers } from "sinon"; -import supertest = require("supertest"); -import { createApp } from "../../../emulator/auth/server"; -import { AgentProjectState } from "../../../emulator/auth/state"; - -export const PROJECT_ID = "example"; - -/** - * Describe a test suite about the Auth Emulator, with server setup properly. - * @param title the title of the test suite - * @param fn the callback where the suite is defined - * @return the mocha test suite - */ -export function describeAuthEmulator( - title: string, - fn: (this: Suite, utils: AuthTestUtils) => void -): Suite { - return describe(`Auth Emulator: ${title}`, function (this) { - let authApp: Express.Application; - beforeEach("setup or reuse auth server", async function (this) { - this.timeout(10000); - authApp = await createOrReuseApp(); - }); - - let clock: sinon.SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); - afterEach(() => clock.restore()); - return fn.call(this, { authApi: () => supertest(authApp), getClock: () => clock }); - }); -} - -export type TestAgent = supertest.SuperTest; - -export type AuthTestUtils = { - authApi: () => TestAgent; - getClock: () => sinon.SinonFakeTimers; -}; - -// Keep a global auth server since start-up takes too long: -let cachedAuthApp: Express.Application; -const projectStateForId = new Map(); - -async function createOrReuseApp(): Promise { - if (!cachedAuthApp) { - cachedAuthApp = await createApp(PROJECT_ID, projectStateForId); - } - // Clear the state every time to make it work like brand new. - // NOTE: This probably won't work with parallel mode if we ever enable it. - projectStateForId.clear(); - return cachedAuthApp; -} diff --git a/src/test/emulators/auth/signUp.spec.ts b/src/test/emulators/auth/signUp.spec.ts deleted file mode 100644 index cc768d6a86d..00000000000 --- a/src/test/emulators/auth/signUp.spec.ts +++ /dev/null @@ -1,591 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator, PROJECT_ID } from "./setup"; -import { - expectStatusCode, - getAccountInfoByIdToken, - getAccountInfoByLocalId, - registerUser, - signInWithFakeClaims, - registerAnonUser, - signInWithPhoneNumber, - updateAccountByLocalId, - getSigninMethods, - TEST_MFA_INFO, - TEST_PHONE_NUMBER, - TEST_PHONE_NUMBER_2, - TEST_INVALID_PHONE_NUMBER, - updateProjectConfig, - registerTenant, -} from "./helpers"; - -describeAuthEmulator("accounts:signUp", ({ authApi }) => { - it("should throw error if no email provided", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ password: "notasecret" /* no email */ }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_EMAIL"); - }); - }); - - it("should issue idToken and refreshToken on anon signUp", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ returnSecureToken: true }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("refreshToken").that.is.a("string"); - - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - expect(decoded!.payload.user_id).to.be.a("string"); - expect(decoded!.payload.provider_id).equals("anonymous"); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("anonymous"); - }); - }); - - it("should issue refreshToken on email+password signUp", async () => { - const email = "me@example.com"; - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ email, password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("refreshToken").that.is.a("string"); - - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.header.alg).to.eql("none"); - expect(decoded!.payload.user_id).to.be.a("string"); - expect(decoded!.payload).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); - expect(decoded!.payload.firebase.identities).to.eql({ - email: [email], - }); - }); - }); - - it("should ignore displayName and photoUrl for new anon account", async () => { - const user = { - displayName: "Me", - photoUrl: "http://localhost/my-profile.png", - }; - const idToken = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send(user) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.displayName).to.be.undefined; - expect(res.body.photoUrl).to.be.undefined; - return res.body.idToken; - }); - const info = await getAccountInfoByIdToken(authApi(), idToken); - expect(info.displayName).to.be.undefined; - expect(info.photoUrl).to.be.undefined; - }); - - it("should set displayName but ignore photoUrl for new password account", async () => { - const user = { - email: "me@example.com", - password: "notasecret", - displayName: "Me", - photoUrl: "http://localhost/my-profile.png", - }; - const idToken = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send(user) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.displayName).to.equal(user.displayName); - expect(res.body.photoUrl).to.be.undefined; - return res.body.idToken; - }); - const info = await getAccountInfoByIdToken(authApi(), idToken); - expect(info.displayName).to.equal(user.displayName); - expect(info.photoUrl).to.be.undefined; - }); - - it("should disallow duplicate email signUp", async () => { - const user = { email: "bob@example.com", password: "notasecret" }; - await registerUser(authApi(), user); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ email: user.email, password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - // Case variants of a same email address are also considered duplicates. - .send({ email: "BOB@example.com", password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); - }); - }); - - it("should error if another account exists with same email from IDP", async () => { - const email = "alice@example.com"; - await signInWithFakeClaims(authApi(), "google.com", { sub: "123", email }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ email, password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); - }); - }); - - it("should error when email format is invalid", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ email: "not.an.email.address.at.all", password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("INVALID_EMAIL"); - }); - }); - - it("should normalize email address to all lowercase", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ email: "AlIcE@exAMPle.COM", password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.email).equals("alice@example.com"); - }); - }); - - it("should error when password is too short", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ email: "me@example.com", password: "short" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .that.satisfy((str: string) => str.startsWith("WEAK_PASSWORD")); - }); - }); - - it("should error when idToken is provided but email / password is not", async () => { - const { idToken } = await registerAnonUser(authApi()); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ idToken /* no email / password */ }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_EMAIL"); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ idToken, email: "alice@example.com" /* no password */ }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_PASSWORD"); - }); - }); - - it("should link email and password to anon user if idToken is provided", async () => { - const { idToken, localId } = await registerAnonUser(authApi()); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ idToken, email: "alice@example.com", password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); - }); - }); - - it("should link email and password to phone sign-in user", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - const email = "alice@example.com"; - - const { idToken, localId } = await signInWithPhoneNumber(authApi(), phoneNumber); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ idToken, email, password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("password"); - - // The result account should have both phone and email. - expect(decoded!.payload.firebase.identities).to.eql({ - phone: [phoneNumber], - email: [email], - }); - }); - }); - - it("should error if account to be linked is disabled", async () => { - const { idToken, localId } = await registerAnonUser(authApi()); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ idToken, email: "alice@example.com", password: "notasecret" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("USER_DISABLED"); - }); - }); - - it("should replace existing email / password in linked account", async () => { - const oldEmail = "alice@example.com"; - const newEmail = "bob@example.com"; - const oldPassword = "notasecret"; - const newPassword = "notasecret2"; - - const { idToken, localId } = await registerUser(authApi(), { - email: oldEmail, - password: oldPassword, - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ idToken, email: newEmail, password: newPassword }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - expect(res.body.email).to.equal(newEmail); - const idToken = res.body.idToken; - const decoded = decodeJwt(idToken, { complete: true }) as { - header: JwtHeader; - payload: FirebaseJwtPayload; - } | null; - expect(decoded, "JWT returned by emulator is invalid").not.to.be.null; - expect(decoded!.payload.email).to.equal(newEmail); - expect(decoded!.payload.firebase.identities).to.eql({ - email: [newEmail], - }); - }); - - const oldEmailSignInMethods = await getSigninMethods(authApi(), oldEmail); - expect(oldEmailSignInMethods).to.be.empty; - }); - - it("should create new account with phone number when authenticated", async () => { - const phoneNumber = TEST_PHONE_NUMBER; - const displayName = "Alice"; - const localId = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .set("Authorization", "Bearer owner") - .send({ phoneNumber, displayName }) - .then((res) => { - expectStatusCode(200, res); - - // Shouldn't be set for authenticated requests: - expect(res.body).not.to.have.property("idToken"); - expect(res.body).not.to.have.property("refreshToken"); - - expect(res.body.displayName).to.equal(displayName); - expect(res.body.localId).to.be.a("string").and.not.empty; - return res.body.localId as string; - }); - - // This should sign into the same user. - const phoneAuth = await signInWithPhoneNumber(authApi(), phoneNumber); - expect(phoneAuth.localId).to.equal(localId); - - const info = await getAccountInfoByIdToken(authApi(), phoneAuth.idToken); - expect(info.displayName).to.equal(displayName); // should already be set. - }); - - it("should error when extra localId parameter is provided", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .query({ key: "fake-api-key" }) - .send({ localId: "anything" /* cannot be specified since this is unauthenticated */ }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("UNEXPECTED_PARAMETER : User ID"); - }); - - const { idToken, localId } = await registerAnonUser(authApi()); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .set("Authorization", "Bearer owner") - .send({ - idToken, - localId, - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("UNEXPECTED_PARAMETER : User ID"); - }); - }); - - it("should create new account with specified localId when authenticated", async () => { - const localId = "haha"; - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .set("Authorization", "Bearer owner") - .send({ localId }) - .then((res) => { - expectStatusCode(200, res); - - // Shouldn't be set for authenticated requests: - expect(res.body).not.to.have.property("idToken"); - expect(res.body).not.to.have.property("refreshToken"); - - expect(res.body.localId).to.equal(localId); - }); - }); - - it("should error when creating new user with duplicate localId", async () => { - const { localId } = await registerAnonUser(authApi()); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .set("Authorization", "Bearer owner") - .send({ localId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("DUPLICATE_LOCAL_ID"); - }); - }); - - it("should error if phone number is invalid", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .set("Authorization", "Bearer owner") - .send({ phoneNumber: TEST_INVALID_PHONE_NUMBER }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("INVALID_PHONE_NUMBER : Invalid format."); - }); - }); - - it("should create new account with multi factor info", async () => { - const user = { email: "alice@example.com", password: "notasecret", mfaInfo: [TEST_MFA_INFO] }; - const { localId } = await registerUser(authApi(), user); - const info = await getAccountInfoByLocalId(authApi(), localId); - expect(info.mfaInfo).to.have.length(1); - const savedMfaInfo = info.mfaInfo![0]; - expect(savedMfaInfo).to.include(TEST_MFA_INFO); - expect(savedMfaInfo?.mfaEnrollmentId).to.be.a("string").and.not.empty; - }); - - it("should create new account with two MFA factors", async () => { - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO, { ...TEST_MFA_INFO, phoneInfo: TEST_PHONE_NUMBER_2 }], - }; - const { localId } = await registerUser(authApi(), user); - const info = await getAccountInfoByLocalId(authApi(), localId); - expect(info.mfaInfo).to.have.length(2); - for (const savedMfaInfo of info.mfaInfo!) { - if (savedMfaInfo.phoneInfo !== TEST_MFA_INFO.phoneInfo) { - expect(savedMfaInfo.phoneInfo).to.eq(TEST_PHONE_NUMBER_2); - } else { - expect(savedMfaInfo).to.include(TEST_MFA_INFO); - } - expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; - } - }); - - it("should de-duplicate factors with the same info on create", async () => { - const alice = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO, TEST_MFA_INFO, TEST_MFA_INFO], - }; - const { localId: aliceLocalId } = await registerUser(authApi(), alice); - const aliceInfo = await getAccountInfoByLocalId(authApi(), aliceLocalId); - expect(aliceInfo.mfaInfo).to.have.length(1); - expect(aliceInfo.mfaInfo![0]).to.include(TEST_MFA_INFO); - expect(aliceInfo.mfaInfo![0].mfaEnrollmentId).to.be.a("string").and.not.empty; - - const bob = { - email: "bob@example.com", - password: "notasecret", - mfaInfo: [ - TEST_MFA_INFO, - TEST_MFA_INFO, - TEST_MFA_INFO, - { ...TEST_MFA_INFO, phoneInfo: TEST_PHONE_NUMBER_2 }, - ], - }; - const { localId: bobLocalId } = await registerUser(authApi(), bob); - const bobInfo = await getAccountInfoByLocalId(authApi(), bobLocalId); - expect(bobInfo.mfaInfo).to.have.length(2); - for (const savedMfaInfo of bobInfo.mfaInfo!) { - if (savedMfaInfo.phoneInfo !== TEST_MFA_INFO.phoneInfo) { - expect(savedMfaInfo.phoneInfo).to.eq(TEST_PHONE_NUMBER_2); - } else { - expect(savedMfaInfo).to.include(TEST_MFA_INFO); - } - expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; - } - }); - - it("does not require a display name for multi factor info", async () => { - const mfaInfo = { phoneInfo: TEST_PHONE_NUMBER }; - const user = { email: "alice@example.com", password: "notasecret", mfaInfo: [mfaInfo] }; - const { localId } = await registerUser(authApi(), user); - - const info = await getAccountInfoByLocalId(authApi(), localId); - expect(info.mfaInfo).to.have.length(1); - const savedMfaInfo = info.mfaInfo![0]; - expect(savedMfaInfo).to.include(mfaInfo); - expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; - expect(savedMfaInfo.displayName).to.be.undefined; - }); - - it("should error if multi factor phone number is invalid", async () => { - const mfaInfo = { phoneInfo: TEST_INVALID_PHONE_NUMBER }; - const user = { email: "alice@example.com", password: "notasecret", mfaInfo: [mfaInfo] }; - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .set("Authorization", "Bearer owner") - .send(user) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("INVALID_MFA_PHONE_NUMBER : Invalid format."); - }); - }); - - it("should ignore if multi factor enrollment ID is specified on create", async () => { - const mfaEnrollmentId1 = "thisShouldBeIgnored1"; - const mfaEnrollmentId2 = "thisShouldBeIgnored2"; - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [ - { - ...TEST_MFA_INFO, - mfaEnrollmentId: mfaEnrollmentId1, - }, - { - ...TEST_MFA_INFO, - mfaEnrollmentId: mfaEnrollmentId2, - }, - ], - }; - const { localId } = await registerUser(authApi(), user); - const info = await getAccountInfoByLocalId(authApi(), localId); - expect(info.mfaInfo).to.have.length(1); - const savedMfaInfo = info.mfaInfo![0]; - expect(savedMfaInfo).to.include(TEST_MFA_INFO); - expect(savedMfaInfo.mfaEnrollmentId).to.be.a("string").and.not.empty; - expect([mfaEnrollmentId1, mfaEnrollmentId2]).not.to.include(savedMfaInfo.mfaEnrollmentId); - }); - - it("should error on signUp if usageMode is passthrough", async () => { - await updateProjectConfig(authApi(), { usageMode: "PASSTHROUGH" }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ returnSecureToken: true }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals("UNSUPPORTED_PASSTHROUGH_OPERATION"); - }); - }); - - it("should error if auth is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").includes("PROJECT_DISABLED"); - }); - }); - - it("should error if password sign up is not allowed", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { allowPasswordSignup: false }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId, email: "me@example.com", password: "notasecret" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").includes("OPERATION_NOT_ALLOWED"); - }); - }); - - it("should error if anonymous user is disabled", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { enableAnonymousUser: false }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .query({ key: "fake-api-key" }) - .send({ tenantId: tenant.tenantId, returnSecureToken: true }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").includes("ADMIN_ONLY_OPERATION"); - }); - }); - - it("should create new account with tenant info", async () => { - const tenant = await registerTenant(authApi(), PROJECT_ID, { allowPasswordSignup: true }); - const user = { tenantId: tenant.tenantId, email: "alice@example.com", password: "notasecret" }; - - const localId = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .query({ key: "fake-api-key" }) - .send(user) - .then((res) => { - expectStatusCode(200, res); - return res.body.localId; - }); - const info = await getAccountInfoByLocalId(authApi(), localId, tenant.tenantId); - - expect(info.tenantId).to.eql(tenant.tenantId); - }); -}); diff --git a/src/test/emulators/cloudFunctions.spec.ts b/src/test/emulators/cloudFunctions.spec.ts deleted file mode 100644 index 83a9571a435..00000000000 --- a/src/test/emulators/cloudFunctions.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect } from "chai"; -import * as nock from "nock"; -import * as sinon from "sinon"; - -import { AuthCloudFunction } from "../../emulator/auth/cloudFunctions"; -import { EmulatorRegistry } from "../../emulator/registry"; -import { Emulators } from "../../emulator/types"; -import { FakeEmulator } from "./fakeEmulator"; - -describe("cloudFunctions", () => { - describe("dispatch", () => { - let sandbox: sinon.SinonSandbox; - const fakeEmulator = new FakeEmulator(Emulators.FUNCTIONS, "1.1.1.1", 4); - before(() => { - sandbox = sinon.createSandbox(); - sandbox.stub(EmulatorRegistry, "get").returns(fakeEmulator); - }); - - after(() => { - sandbox.restore(); - nock.cleanAll(); - }); - - it("should make a request to the functions emulator", async () => { - nock("http://1.1.1.1:4") - .post("/functions/projects/project-foo/trigger_multicast", { - eventId: /.*/, - eventType: "providers/firebase.auth/eventTypes/user.create", - resource: { - name: "projects/project-foo", - service: "firebaseauth.googleapis.com", - }, - params: {}, - timestamp: /.*/, - data: { uid: "foobar", metadata: {}, customClaims: {} }, - }) - .reply(200, {}); - - const cf = new AuthCloudFunction("project-foo"); - await cf.dispatch("create", { localId: "foobar" }); - expect(nock.isDone()).to.be.true; - }); - }); -}); diff --git a/src/test/emulators/commandUtils.spec.ts b/src/test/emulators/commandUtils.spec.ts deleted file mode 100644 index b3336794a86..00000000000 --- a/src/test/emulators/commandUtils.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as commandUtils from "../../emulator/commandUtils"; -import { expect } from "chai"; -import { FirebaseError } from "../../error"; -import { EXPORT_ON_EXIT_USAGE_ERROR } from "../../emulator/commandUtils"; - -describe("commandUtils", () => { - const testSetExportOnExitOptions = (options: any): any => { - commandUtils.setExportOnExitOptions(options); - return options; - }; - - it("should validate --export-on-exit options", () => { - expect(testSetExportOnExitOptions({ import: "./data" }).exportOnExit).to.be.undefined; - expect( - testSetExportOnExitOptions({ import: "./data", exportOnExit: "./data" }).exportOnExit - ).to.eql("./data"); - expect( - testSetExportOnExitOptions({ import: "./data", exportOnExit: "./dataExport" }).exportOnExit - ).to.eql("./dataExport"); - expect( - testSetExportOnExitOptions({ import: "./data", exportOnExit: true }).exportOnExit - ).to.eql("./data"); - expect(() => testSetExportOnExitOptions({ exportOnExit: true })).to.throw( - FirebaseError, - EXPORT_ON_EXIT_USAGE_ERROR - ); - expect(() => testSetExportOnExitOptions({ import: "", exportOnExit: true })).to.throw( - FirebaseError, - EXPORT_ON_EXIT_USAGE_ERROR - ); - expect(() => testSetExportOnExitOptions({ import: "", exportOnExit: "" })).to.throw( - FirebaseError, - EXPORT_ON_EXIT_USAGE_ERROR - ); - }); - it("should delete the --import option when the dir does not exist together with --export-on-exit", () => { - expect( - testSetExportOnExitOptions({ - import: "./dataDirThatDoesNotExist", - exportOnExit: "./dataDirThatDoesNotExist", - }).import - ).to.be.undefined; - const options = testSetExportOnExitOptions({ - import: "./dataDirThatDoesNotExist", - exportOnExit: true, - }); - expect(options.import).to.be.undefined; - expect(options.exportOnExit).to.eql("./dataDirThatDoesNotExist"); - }); - it("should not touch the --import option when the dir does not exist but --export-on-exit is not set", () => { - expect( - testSetExportOnExitOptions({ - import: "./dataDirThatDoesNotExist", - }).import - ).to.eql("./dataDirThatDoesNotExist"); - }); - it("should keep other unrelated options when using setExportOnExitOptions", () => { - expect( - testSetExportOnExitOptions({ - someUnrelatedOption: "isHere", - }).someUnrelatedOption - ).to.eql("isHere"); - }); -}); diff --git a/src/test/emulators/controller.spec.ts b/src/test/emulators/controller.spec.ts deleted file mode 100644 index bbfcb2acb3d..00000000000 --- a/src/test/emulators/controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Emulators } from "../../emulator/types"; -import { startEmulator } from "../../emulator/controller"; -import { EmulatorRegistry } from "../../emulator/registry"; -import { expect } from "chai"; -import { FakeEmulator } from "./fakeEmulator"; - -describe("EmulatorController", () => { - afterEach(async () => { - await EmulatorRegistry.stopAll(); - }); - - it("should start and stop an emulator", async () => { - const name = Emulators.FUNCTIONS; - - expect(EmulatorRegistry.isRunning(name)).to.be.false; - - await startEmulator(new FakeEmulator(name, "localhost", 7777)); - - expect(EmulatorRegistry.isRunning(name)).to.be.true; - expect(EmulatorRegistry.getPort(name)).to.eql(7777); - }); -}); diff --git a/src/test/emulators/downloadableEmulators.spec.ts b/src/test/emulators/downloadableEmulators.spec.ts deleted file mode 100644 index 551381a6078..00000000000 --- a/src/test/emulators/downloadableEmulators.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { expect } from "chai"; -import * as path from "path"; - -import * as downloadableEmulators from "../../emulator/downloadableEmulators"; -import { Emulators } from "../../emulator/types"; - -type DownloadableEmulator = Emulators.FIRESTORE | Emulators.DATABASE | Emulators.PUBSUB; - -function checkDownloadPath(name: DownloadableEmulator): void { - const emulator = downloadableEmulators.getDownloadDetails(name); - expect(path.basename(emulator.opts.remoteUrl)).to.eq(path.basename(emulator.downloadPath)); -} - -describe("downloadDetails", () => { - it("should match the basename of remoteUrl", () => { - checkDownloadPath(Emulators.FIRESTORE); - checkDownloadPath(Emulators.DATABASE); - checkDownloadPath(Emulators.PUBSUB); - }); -}); diff --git a/src/test/emulators/emulatorServer.spec.ts b/src/test/emulators/emulatorServer.spec.ts deleted file mode 100644 index d1c28d54c6f..00000000000 --- a/src/test/emulators/emulatorServer.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Emulators } from "../../emulator/types"; -import { EmulatorRegistry } from "../../emulator/registry"; -import { expect } from "chai"; -import { FakeEmulator } from "./fakeEmulator"; -import { EmulatorServer } from "../../emulator/emulatorServer"; - -describe("EmulatorServer", () => { - it("should correctly start and stop an emulator", async () => { - const name = Emulators.FUNCTIONS; - const emulator = new FakeEmulator(name, "localhost", 5000); - const server = new EmulatorServer(emulator); - - await server.start(); - - expect(EmulatorRegistry.isRunning(name)).to.be.true; - expect(EmulatorRegistry.get(name)).to.eql(emulator); - - await server.stop(); - - expect(EmulatorRegistry.isRunning(name)).to.be.false; - }); -}); diff --git a/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/extension.yaml b/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/extension.yaml new file mode 100644 index 00000000000..dbda3da8e4a --- /dev/null +++ b/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/extension.yaml @@ -0,0 +1,226 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: storage-resize-images +version: 0.1.18 +specVersion: v1beta + +displayName: Resize Images +description: Resizes images uploaded to Cloud Storage to a specified size, and optionally keeps or deletes the original image. + +license: Apache-2.0 + +sourceUrl: https://github.com/firebase/extensions/tree/master/storage-resize-images +releaseNotesUrl: https://github.com/firebase/extensions/blob/master/storage-resize-images/CHANGELOG.md + +author: + authorName: Firebase + url: https://firebase.google.com + +contributors: + - authorName: Tina Liang + url: https://github.com/tinaliang + - authorName: Chris Bianca + email: chris@csfrequency.com + url: https://github.com/chrisbianca + - authorName: Invertase + email: oss@invertase.io + url: https://github.com/invertase + +billingRequired: true + +apis: + - apiName: storage-component.googleapis.com + reason: Needed to use Cloud Storage + +roles: + - role: storage.admin + reason: Allows the extension to store resized images in Cloud Storage + +resources: + - name: generateResizedImage + type: firebaseextensions.v1beta.function + description: >- + Listens for new images uploaded to your specified Cloud Storage bucket, resizes the images, + then stores the resized images in the same bucket. Optionally keeps or deletes the original images. + properties: + location: ${param:LOCATION} + runtime: nodejs10 + eventTrigger: + eventType: google.storage.object.finalize + resource: projects/_/buckets/${param:IMG_BUCKET} + +params: + - param: LOCATION + label: Cloud Functions location + description: >- + Where do you want to deploy the functions created for this extension? + You usually want a location close to your Storage bucket. For help selecting a + location, refer to the [location selection + guide](https://firebase.google.com/docs/functions/locations). + type: select + options: + - label: Iowa (us-central1) + value: us-central1 + - label: South Carolina (us-east1) + value: us-east1 + - label: Northern Virginia (us-east4) + value: us-east4 + - label: Los Angeles (us-west2) + value: us-west2 + - label: Salt Lake City (us-west3) + value: us-west3 + - label: Las Vegas (us-west4) + value: us-west4 + - label: Belgium (europe-west1) + value: europe-west1 + - label: London (europe-west2) + value: europe-west2 + - label: Frankfurt (europe-west3) + value: europe-west3 + - label: Zurich (europe-west6) + value: europe-west6 + - label: Hong Kong (asia-east2) + value: asia-east2 + - label: Tokyo (asia-northeast1) + value: asia-northeast1 + - label: Osaka (asia-northeast2) + value: asia-northeast2 + - label: Seoul (asia-northeast3) + value: asia-northeast3 + - label: Mumbai (asia-south1) + value: asia-south1 + - label: Jakarta (asia-southeast2) + value: asia-southeast2 + - label: Montreal (northamerica-northeast1) + value: northamerica-northeast1 + - label: Sao Paulo (southamerica-east1) + value: southamerica-east1 + - label: Sydney (australia-southeast1) + value: australia-southeast1 + default: us-central1 + required: true + immutable: true + + - param: IMG_BUCKET + label: Cloud Storage bucket for images + description: > + To which Cloud Storage bucket will you upload images that you want to resize? + Resized images will be stored in this bucket. Depending on your extension configuration, + original images are either kept or deleted. + type: string + example: my-project-12345.appspot.com + validationRegex: ^([0-9a-z_.-]*)$ + validationErrorMessage: Invalid storage bucket + default: ${STORAGE_BUCKET} + required: true + + - param: IMG_SIZES + label: Sizes of resized images + description: > + What sizes of images would you like (in pixels)? Enter the sizes as a + comma-separated list of WIDTHxHEIGHT values. Learn more about + [how this parameter works](https://firebase.google.com/products/extensions/storage-resize-images). + type: string + example: "200x200" + validationRegex: ^\d+x(\d+,\d+x)*\d+$ + validationErrorMessage: Invalid sizes, must be a comma-separated list of WIDTHxHEIGHT values. + default: "200x200" + required: true + + - param: DELETE_ORIGINAL_FILE + label: Deletion of original file + description: >- + Do you want to automatically delete the original file from the Cloud Storage + bucket? Note that these deletions cannot be undone. + type: select + options: + - label: Yes + value: true + - label: No + value: false + - label: Delete on successful resize + value: on_success + default: false + required: true + + - param: RESIZED_IMAGES_PATH + label: Cloud Storage path for resized images + description: > + A relative path in which to store resized images. For example, + if you specify a path here of `thumbs` and you upload an image to + `/images/original.jpg`, then the resized image is stored at + `/images/thumbs/original_200x200.jpg`. If you prefer to store resized + images at the root of your bucket, leave this field empty. + example: thumbnails + required: false + + - param: INCLUDE_PATH_LIST + label: Paths that contain images you want to resize + description: > + Restrict storage-resize-images to only resize images in specific locations in your Storage bucket by + supplying a comma-separated list of absolute paths. For example, to only resize the images + stored in `/users/pictures` directory, specify the path `/users/pictures`. + If you prefer to resize every image uploaded to the storage bucket, + leave this field empty. + type: string + example: "/users/avatars,/design/pictures" + validationRegex: ^(\/[^\s\/\,]+)+(\,(\/[^\s\/\,]+)+)*$ + validationErrorMessage: Invalid paths, must be a comma-separated list of absolute path values. + required: false + + - param: EXCLUDE_PATH_LIST + label: List of absolute paths not included for resized images + description: > + A comma-separated list of absolute paths to not take into account for + images to be resized. For example, to not resize the images + stored in `/users/pictures/avatars` directory, specify the path + `/users/pictures/avatars`. If you prefer to resize every image uploaded + to the storage bucket, leave this field empty. + type: string + example: "/users/avatars/thumbs,/design/pictures/thumbs" + validationRegex: ^(\/[^\s\/\,]+)+(\,(\/[^\s\/\,]+)+)*$ + validationErrorMessage: Invalid paths, must be a comma-separated list of absolute path values. + required: false + + - param: CACHE_CONTROL_HEADER + label: Cache-Control header for resized images + description: > + This extension automatically copies any `Cache-Control` metadata from the original image + to the resized images. For the resized images, do you want to overwrite this copied + `Cache-Control` metadata or add `Cache-Control` metadata? Learn more about + [`Cache-Control` headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control). + If you prefer not to overwrite or add `Cache-Control` metadata, leave this field empty. + example: max-age=86400 + required: false + + - param: IMAGE_TYPE + label: Convert image to preferred type + description: > + The image type you'd like your source image to convert to. The default for this option will + be to keep the original file type. + type: select + options: + - label: jpg + value: jpg + - label: png + value: png + - label: webp + value: webp + - label: tiff + value: tiff + - label: Do not convert + value: false + default: false + required: false diff --git a/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/.gitignore b/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/.gitignore new file mode 100644 index 00000000000..9dc671d9430 --- /dev/null +++ b/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/.gitignore @@ -0,0 +1,2 @@ +#include node_modules here for testing. +!/node_modules diff --git a/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/package.json b/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/package.json new file mode 100644 index 00000000000..e8ba8a23d5b --- /dev/null +++ b/src/test/emulators/extensions/firebase/storage-resize-images@0.1.18/functions/package.json @@ -0,0 +1,5 @@ +{ + "name": "storage-resize-images", + "vresion": "0.1.18", + "description": "Package file for testing only" +} diff --git a/src/test/emulators/fakeEmulator.ts b/src/test/emulators/fakeEmulator.ts deleted file mode 100644 index 5321d375a57..00000000000 --- a/src/test/emulators/fakeEmulator.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { EmulatorInfo, EmulatorInstance, Emulators } from "../../emulator/types"; -import * as express from "express"; -import { createDestroyer } from "../../utils"; - -/** - * A thing that acts like an emulator by just occupying a port. - */ -export class FakeEmulator implements EmulatorInstance { - private exp: express.Express; - private destroyServer?: () => Promise; - - constructor(public name: Emulators, public host: string, public port: number) { - this.exp = express(); - } - - start(): Promise { - const server = this.exp.listen(this.port); - this.destroyServer = createDestroyer(server); - return Promise.resolve(); - } - connect(): Promise { - return Promise.resolve(); - } - stop(): Promise { - return this.destroyServer ? this.destroyServer() : Promise.resolve(); - } - getInfo(): EmulatorInfo { - return { - name: this.getName(), - host: this.host, - port: this.port, - }; - } - getName(): Emulators { - return this.name; - } -} diff --git a/src/test/emulators/functionsEmulatorShared.spec.ts b/src/test/emulators/functionsEmulatorShared.spec.ts deleted file mode 100644 index 4459322695f..00000000000 --- a/src/test/emulators/functionsEmulatorShared.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { expect } from "chai"; -import { getFunctionService } from "../../emulator/functionsEmulatorShared"; - -const baseDef = { - platform: "gcfv1" as const, - id: "trigger-id", - region: "us-central1", - entryPoint: "fn", - name: "name", -}; - -describe("FunctionsEmulatorShared", () => { - describe("getFunctionService", () => { - it("should get service from event trigger definition", () => { - const def = { - ...baseDef, - eventTrigger: { - resource: "projects/my-project/topics/my-topic", - eventType: "google.cloud.pubsub.topic.v1.messagePublished", - service: "pubsub.googleapis.com", - }, - }; - expect(getFunctionService(def)).to.be.eql("pubsub.googleapis.com"); - }); - - it("should return unknown if trigger definition is not event-based", () => { - const def = { - ...baseDef, - httpsTrigger: {}, - }; - expect(getFunctionService(def)).to.be.eql("unknown"); - }); - - it("should infer pubsub service based on eventType", () => { - const def = { - ...baseDef, - eventTrigger: { - resource: "projects/my-project/topics/my-topic", - eventType: "google.cloud.pubsub.topic.v1.messagePublished", - }, - }; - expect(getFunctionService(def)).to.be.eql("pubsub.googleapis.com"); - }); - - it("should infer firestore service based on eventType", () => { - const def = { - ...baseDef, - eventTrigger: { - resource: "projects/my-project/databases/(default)/documents/my-collection/{docId}", - eventType: "providers/cloud.firestore/eventTypes/document.write", - }, - }; - expect(getFunctionService(def)).to.be.eql("firestore.googleapis.com"); - }); - - it("should infer database service based on eventType", () => { - const def = { - ...baseDef, - eventTrigger: { - resource: "projects/_/instances/my-project/refs/messages/{pushId}", - eventType: "providers/google.firebase.database/eventTypes/ref.write", - }, - }; - expect(getFunctionService(def)).to.be.eql("firebaseio.com"); - }); - - it("should infer storage service based on eventType", () => { - const def = { - ...baseDef, - eventTrigger: { - resource: "projects/_/buckets/mybucket", - eventType: "google.storage.object.finalize", - }, - }; - expect(getFunctionService(def)).to.be.eql("storage.googleapis.com"); - }); - - it("should infer auth service based on eventType", () => { - const def = { - ...baseDef, - eventTrigger: { - resource: "projects/my-project", - eventType: "providers/firebase.auth/eventTypes/user.create", - }, - }; - expect(getFunctionService(def)).to.be.eql("firebaseauth.googleapis.com"); - }); - }); -}); diff --git a/src/test/emulators/functionsEmulatorUtils.spec.ts b/src/test/emulators/functionsEmulatorUtils.spec.ts deleted file mode 100644 index c82aa59a5d6..00000000000 --- a/src/test/emulators/functionsEmulatorUtils.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { expect } from "chai"; -import { - extractParamsFromPath, - isValidWildcardMatch, - trimSlashes, - compareVersionStrings, - parseRuntimeVersion, -} from "../../emulator/functionsEmulatorUtils"; - -describe("FunctionsEmulatorUtils", () => { - describe("extractParamsFromPath", () => { - it("should match a path which fits a wildcard template", () => { - const params = extractParamsFromPath( - "companies/{company}/users/{user}", - "/companies/firebase/users/abe" - ); - expect(params).to.deep.equal({ company: "firebase", user: "abe" }); - }); - - it("should not match unfilled wildcards", () => { - const params = extractParamsFromPath( - "companies/{company}/users/{user}", - "companies/{still_wild}/users/abe" - ); - expect(params).to.deep.equal({ user: "abe" }); - }); - - it("should not match a path which is too long", () => { - const params = extractParamsFromPath( - "companies/{company}/users/{user}", - "companies/firebase/users/abe/boots" - ); - expect(params).to.deep.equal({}); - }); - - it("should not match a path which is too short", () => { - const params = extractParamsFromPath( - "companies/{company}/users/{user}", - "companies/firebase/users/" - ); - expect(params).to.deep.equal({}); - }); - - it("should not match a path which has different chunks", () => { - const params = extractParamsFromPath( - "locations/{company}/users/{user}", - "companies/firebase/users/{user}" - ); - expect(params).to.deep.equal({}); - }); - }); - - describe("isValidWildcardMatch", () => { - it("should match a path which fits a wildcard template", () => { - const valid = isValidWildcardMatch( - "companies/{company}/users/{user}", - "/companies/firebase/users/abe" - ); - expect(valid).to.equal(true); - }); - - it("should not match a path which is too long", () => { - const tooLong = isValidWildcardMatch( - "companies/{company}/users/{user}", - "companies/firebase/users/abe/boots" - ); - expect(tooLong).to.equal(false); - }); - - it("should not match a path which is too short", () => { - const tooShort = isValidWildcardMatch( - "companies/{company}/users/{user}", - "companies/firebase/users/" - ); - expect(tooShort).to.equal(false); - }); - - it("should not match a path which has different chunks", () => { - const differentChunk = isValidWildcardMatch( - "locations/{company}/users/{user}", - "companies/firebase/users/{user}" - ); - expect(differentChunk).to.equal(false); - }); - }); - - describe("trimSlashes", () => { - it("should remove leading and trailing slashes", () => { - expect(trimSlashes("///a/b/c////")).to.equal("a/b/c"); - }); - it("should replace multiple adjacent slashes with a single slash", () => { - expect(trimSlashes("a////b//c")).to.equal("a/b/c"); - }); - it("should do both", () => { - expect(trimSlashes("///a////b//c/")).to.equal("a/b/c"); - }); - }); - - describe("compareVersonStrings", () => { - it("should detect a higher major version", () => { - expect(compareVersionStrings("4.0.0", "3.2.1")).to.be.gt(0); - expect(compareVersionStrings("3.2.1", "4.0.0")).to.be.lt(0); - }); - - it("should detect a higher minor version", () => { - expect(compareVersionStrings("4.1.0", "4.0.1")).to.be.gt(0); - expect(compareVersionStrings("4.0.1", "4.1.0")).to.be.lt(0); - }); - - it("should detect a higher patch version", () => { - expect(compareVersionStrings("4.0.1", "4.0.0")).to.be.gt(0); - expect(compareVersionStrings("4.0.0", "4.0.1")).to.be.lt(0); - }); - - it("should detect the same version", () => { - expect(compareVersionStrings("4.0.0", "4.0.0")).to.eql(0); - expect(compareVersionStrings("4.0", "4.0.0")).to.eql(0); - expect(compareVersionStrings("4", "4.0.0")).to.eql(0); - }); - }); - - describe("parseRuntimeVerson", () => { - it("should parse fully specified runtime strings", () => { - expect(parseRuntimeVersion("nodejs6")).to.eql(6); - expect(parseRuntimeVersion("nodejs8")).to.eql(8); - expect(parseRuntimeVersion("nodejs10")).to.eql(10); - expect(parseRuntimeVersion("nodejs12")).to.eql(12); - }); - - it("should parse plain number strings", () => { - expect(parseRuntimeVersion("6")).to.eql(6); - expect(parseRuntimeVersion("8")).to.eql(8); - expect(parseRuntimeVersion("10")).to.eql(10); - expect(parseRuntimeVersion("12")).to.eql(12); - }); - - it("should ignore unknown", () => { - expect(parseRuntimeVersion("banana")).to.eql(undefined); - }); - }); -}); diff --git a/src/test/emulators/functionsRuntimeWorker.spec.ts b/src/test/emulators/functionsRuntimeWorker.spec.ts deleted file mode 100644 index 759e4baf2d0..00000000000 --- a/src/test/emulators/functionsRuntimeWorker.spec.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { expect } from "chai"; -import { FunctionsRuntimeInstance } from "../../emulator/functionsEmulator"; -import { EventEmitter } from "events"; -import { - FunctionsRuntimeArgs, - FunctionsRuntimeBundle, -} from "../../emulator/functionsEmulatorShared"; -import { - RuntimeWorker, - RuntimeWorkerState, - RuntimeWorkerPool, -} from "../../emulator/functionsRuntimeWorker"; -import { FunctionsExecutionMode } from "../../emulator/types"; - -/** - * Fake runtime instance we can use to simulate different subprocess conditions. - * It automatically fails or succeeds 10ms after being given work to do. - */ -class MockRuntimeInstance implements FunctionsRuntimeInstance { - pid: number = 12345; - metadata: { [key: string]: any } = {}; - events: EventEmitter = new EventEmitter(); - exit: Promise; - - constructor(private success: boolean) { - this.exit = new Promise((res) => { - this.events.on("exit", res); - }); - } - - shutdown(): void { - this.events.emit("exit", { reason: "shutdown" }); - } - - kill(signal?: string): void { - this.events.emit("exit", { reason: "kill" }); - } - - send(args: FunctionsRuntimeArgs): boolean { - setTimeout(() => { - if (this.success) { - this.logRuntimeStatus({ state: "idle" }); - } else { - this.kill(); - } - }, 10); - return true; - } - - logRuntimeStatus(data: any) { - this.events.emit("log", { type: "runtime-status", data }); - } -} - -/** - * Test helper to count worker state transitions. - */ -class WorkerStateCounter { - counts: { [state in RuntimeWorkerState]: number } = { - IDLE: 0, - BUSY: 0, - FINISHING: 0, - FINISHED: 0, - }; - - constructor(worker: RuntimeWorker) { - this.increment(worker.state); - worker.stateEvents.on(RuntimeWorkerState.IDLE, () => { - this.increment(RuntimeWorkerState.IDLE); - }); - worker.stateEvents.on(RuntimeWorkerState.BUSY, () => { - this.increment(RuntimeWorkerState.BUSY); - }); - worker.stateEvents.on(RuntimeWorkerState.FINISHING, () => { - this.increment(RuntimeWorkerState.FINISHING); - }); - worker.stateEvents.on(RuntimeWorkerState.FINISHED, () => { - this.increment(RuntimeWorkerState.FINISHED); - }); - } - - private increment(state: RuntimeWorkerState) { - this.counts[state]++; - } - - get total() { - return this.counts.IDLE + this.counts.BUSY + this.counts.FINISHING + this.counts.FINISHED; - } -} - -class MockRuntimeBundle implements FunctionsRuntimeBundle { - projectId = "project-1234"; - cwd = "/home/users/dir"; - emulators = {}; - adminSdkConfig = { - projectId: "project-1234", - datbaseURL: "https://project-1234-default-rtdb.firebaseio.com", - storageBucket: "project-1234.appspot.com", - }; - - constructor(public triggerId: string, public targetName: string) {} -} - -describe("FunctionsRuntimeWorker", () => { - const workerPool = new RuntimeWorkerPool(); - - describe("RuntimeWorker", () => { - it("goes from idle --> busy --> idle in normal operation", async () => { - const worker = new RuntimeWorker(workerPool.getKey("trigger"), new MockRuntimeInstance(true)); - const counter = new WorkerStateCounter(worker); - - worker.execute(new MockRuntimeBundle("region-trigger", "trigger-name")); - await worker.waitForDone(); - - expect(counter.counts.BUSY).to.eql(1); - expect(counter.counts.IDLE).to.eql(2); - expect(counter.total).to.eql(3); - }); - - it("goes from idle --> busy --> finished when there's an error", async () => { - const worker = new RuntimeWorker( - workerPool.getKey("trigger"), - new MockRuntimeInstance(false) - ); - const counter = new WorkerStateCounter(worker); - - worker.execute(new MockRuntimeBundle("region-trigger", "trigger-name")); - await worker.waitForDone(); - - expect(counter.counts.IDLE).to.eql(1); - expect(counter.counts.BUSY).to.eql(1); - expect(counter.counts.FINISHED).to.eql(1); - expect(counter.total).to.eql(3); - }); - - it("goes from busy --> finishing --> finished when marked", async () => { - const worker = new RuntimeWorker(workerPool.getKey("trigger"), new MockRuntimeInstance(true)); - const counter = new WorkerStateCounter(worker); - - worker.execute(new MockRuntimeBundle("region-trigger", "trigger-name")); - worker.state = RuntimeWorkerState.FINISHING; - await worker.waitForDone(); - - expect(counter.counts.IDLE).to.eql(1); - expect(counter.counts.BUSY).to.eql(1); - expect(counter.counts.FINISHING).to.eql(1); - expect(counter.counts.FINISHED).to.eql(1); - expect(counter.total).to.eql(4); - }); - }); - - describe("RuntimeWorkerPool", () => { - it("properly manages a single worker", async () => { - const pool = new RuntimeWorkerPool(); - const trigger = "region-trigger1"; - - // No idle workers to begin - expect(pool.getIdleWorker(trigger)).to.be.undefined; - - // Add a worker and make sure it's there - const worker = pool.addWorker(trigger, new MockRuntimeInstance(true)); - const triggerWorkers = pool.getTriggerWorkers(trigger); - expect(triggerWorkers.length).length.to.eq(1); - expect(pool.getIdleWorker(trigger)).to.eql(worker); - - // Make the worker busy, confirm nothing is idle - worker.execute(new MockRuntimeBundle(trigger, "targetName")); - expect(pool.getIdleWorker(trigger)).to.be.undefined; - - // When the worker is finished work, confirm it's idle again - await worker.waitForDone(); - expect(pool.getIdleWorker(trigger)).to.eql(worker); - }); - - it("does not consider failed workers idle", async () => { - const pool = new RuntimeWorkerPool(); - const trigger = "trigger1"; - - // No idle workers to begin - expect(pool.getIdleWorker(trigger)).to.be.undefined; - - // Add a worker to the pool that will fail, confirm it begins idle - const worker = pool.addWorker(trigger, new MockRuntimeInstance(false)); - expect(pool.getIdleWorker(trigger)).to.eql(worker); - - // Make the worker execute (and fail) - worker.execute(new MockRuntimeBundle(trigger, "targetName")); - await worker.waitForDone(); - - // Confirm there are no idle workers - expect(pool.getIdleWorker(trigger)).to.be.undefined; - }); - - it("exit() kills idle and busy workers", async () => { - const pool = new RuntimeWorkerPool(); - const trigger = "trigger1"; - - const busyWorker = pool.addWorker(trigger, new MockRuntimeInstance(true)); - const busyWorkerCounter = new WorkerStateCounter(busyWorker); - - const idleWorker = pool.addWorker(trigger, new MockRuntimeInstance(true)); - const idleWorkerCounter = new WorkerStateCounter(idleWorker); - - busyWorker.execute(new MockRuntimeBundle(trigger, "targetName")); - pool.exit(); - - await busyWorker.waitForDone(); - await idleWorker.waitForDone(); - - expect(busyWorkerCounter.counts.IDLE).to.eql(1); - expect(busyWorkerCounter.counts.BUSY).to.eql(1); - expect(busyWorkerCounter.counts.FINISHED).to.eql(1); - expect(busyWorkerCounter.total).to.eql(3); - - expect(idleWorkerCounter.counts.IDLE).to.eql(1); - expect(idleWorkerCounter.counts.FINISHED).to.eql(1); - expect(idleWorkerCounter.total).to.eql(2); - }); - - it("refresh() kills idle workers and marks busy ones as finishing", async () => { - const pool = new RuntimeWorkerPool(); - const trigger = "trigger1"; - - const busyWorker = pool.addWorker(trigger, new MockRuntimeInstance(true)); - const busyWorkerCounter = new WorkerStateCounter(busyWorker); - - const idleWorker = pool.addWorker(trigger, new MockRuntimeInstance(true)); - const idleWorkerCounter = new WorkerStateCounter(idleWorker); - - busyWorker.execute(new MockRuntimeBundle(trigger, "targetName")); - pool.refresh(); - - await busyWorker.waitForDone(); - await idleWorker.waitForDone(); - - expect(busyWorkerCounter.counts.BUSY).to.eql(1); - expect(busyWorkerCounter.counts.FINISHING).to.eql(1); - expect(busyWorkerCounter.counts.FINISHED).to.eql(1); - - expect(idleWorkerCounter.counts.IDLE).to.eql(1); - expect(idleWorkerCounter.counts.FINISHING).to.eql(1); - expect(idleWorkerCounter.counts.FINISHED).to.eql(1); - }); - - it("gives assigns all triggers to the same worker in sequential mode", async () => { - const trigger1 = "region-abc"; - const trigger2 = "region-def"; - - const pool = new RuntimeWorkerPool(FunctionsExecutionMode.SEQUENTIAL); - const worker = pool.addWorker(trigger1, new MockRuntimeInstance(true)); - - pool.submitWork(trigger2, new MockRuntimeBundle(trigger2, "def")); - - expect(pool.readyForWork(trigger1)).to.be.false; - expect(pool.readyForWork(trigger2)).to.be.false; - - await worker.waitForDone(); - - expect(pool.readyForWork(trigger1)).to.be.true; - expect(pool.readyForWork(trigger2)).to.be.true; - }); - }); -}); diff --git a/src/test/emulators/registry.spec.ts b/src/test/emulators/registry.spec.ts deleted file mode 100644 index 4fd31a9c9d4..00000000000 --- a/src/test/emulators/registry.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ALL_EMULATORS, Emulators } from "../../emulator/types"; -import { EmulatorRegistry } from "../../emulator/registry"; -import { expect } from "chai"; -import { FakeEmulator } from "./fakeEmulator"; - -describe("EmulatorRegistry", () => { - afterEach(async () => { - await EmulatorRegistry.stopAll(); - }); - - it("should not report any running emulators when empty", () => { - for (const name of ALL_EMULATORS) { - expect(EmulatorRegistry.isRunning(name)).to.be.false; - } - - expect(EmulatorRegistry.listRunning()).to.be.empty; - }); - - it("should correctly return information about a running emulator", async () => { - const name = Emulators.FUNCTIONS; - const emu = new FakeEmulator(name, "localhost", 5000); - - expect(EmulatorRegistry.isRunning(name)).to.be.false; - - await EmulatorRegistry.start(emu); - - expect(EmulatorRegistry.isRunning(name)).to.be.true; - expect(EmulatorRegistry.listRunning()).to.eql([name]); - expect(EmulatorRegistry.get(name)).to.eql(emu); - expect(EmulatorRegistry.getPort(name)).to.eql(5000); - }); - - it("once stopped, an emulator is no longer running", async () => { - const name = Emulators.FUNCTIONS; - const emu = new FakeEmulator(name, "localhost", 5000); - - expect(EmulatorRegistry.isRunning(name)).to.be.false; - await EmulatorRegistry.start(emu); - expect(EmulatorRegistry.isRunning(name)).to.be.true; - await EmulatorRegistry.stop(name); - expect(EmulatorRegistry.isRunning(name)).to.be.false; - }); -}); diff --git a/src/test/emulators/storage.rules.spec.ts b/src/test/emulators/storage.rules.spec.ts deleted file mode 100644 index 5dc97f8688e..00000000000 --- a/src/test/emulators/storage.rules.spec.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { RulesetVerificationOpts, StorageRulesRuntime } from "../../emulator/storage/rules/runtime"; -import { expect } from "chai"; -import { StorageRulesFiles } from "./fixtures"; -import * as jwt from "jsonwebtoken"; -import { EmulatorLogger } from "../../emulator/emulatorLogger"; -import { ExpressionValue } from "../../emulator/storage/rules/expressionValue"; -import { RulesetOperationMethod } from "../../emulator/storage/rules/types"; -import { downloadIfNecessary, getDownloadDetails } from "../../emulator/downloadableEmulators"; -import { Emulators } from "../../emulator/types"; -import { RulesResourceMetadata } from "../../emulator/storage/metadata"; - -const TOKENS = { - signedInUser: jwt.sign( - { - user_id: "mock-user", - }, - "mock-secret" - ), -}; - -function createFakeResourceMetadata(params: { - size?: number; - md5Hash?: string; -}): RulesResourceMetadata { - return { - name: "files/goat", - bucket: "fake-app.appspot.com", - generation: 1, - metageneration: 1, - size: params.size ?? 1024 /* 1 KiB */, - timeCreated: new Date(), - updated: new Date(), - md5Hash: params.md5Hash ?? "fake-md5-hash", - crc32c: "fake-crc32c", - etag: "fake-etag", - contentDisposition: "", - contentEncoding: "", - contentType: "", - metadata: {}, - }; -} - -describe.skip("Storage Rules", function () { - let runtime: StorageRulesRuntime; - - // eslint-disable-next-line @typescript-eslint/no-invalid-this - this.timeout(10000); - - before(async () => { - await downloadIfNecessary(Emulators.STORAGE); - - runtime = new StorageRulesRuntime(); - (EmulatorLogger as any).prototype.log = console.log.bind(console); - await runtime.start(); - }); - - after(() => { - runtime.stop(); - }); - - it("should have a living child process", () => { - expect(runtime.alive).to.be.true; - }); - - it("should load a basic ruleset", async () => { - const { ruleset } = await runtime.loadRuleset({ - files: [StorageRulesFiles.readWriteIfAuth], - }); - - expect(ruleset).to.not.be.undefined; - }); - - it("should send errors on invalid ruleset compilation", async () => { - const { ruleset, issues } = await runtime.loadRuleset({ - files: [ - { - name: "/dev/null/storage.rules", - content: ` - rules_version = '2'; - // Extra brace in the following line - service firebase.storage {{ - match /b/{bucket}/o { - match /{allPaths=**} { - allow read, write: if request.auth!=null; - } - } - } - `, - }, - ], - }); - - expect(ruleset).to.be.undefined; - expect(issues.errors.length).to.gt(0); - }); - - it("should reject an invalid evaluation", async () => { - expect( - await testIfPermitted( - runtime, - ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o { - match /{allPaths=**} { - allow read, write: if false; - } - } - } - `, - { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/num_check/filename.jpg", - file: {}, - } - ) - ).to.be.false; - }); - - it("should accept a value evaluation", async () => { - expect( - await testIfPermitted( - runtime, - ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o { - match /{allPaths=**} { - allow read, write: if true; - } - } - } - `, - { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/num_check/filename.jpg", - file: {}, - } - ) - ).to.be.true; - }); - - describe("request", () => { - describe(".auth", () => { - it("can read from auth.uid", async () => { - expect( - await testIfPermitted( - runtime, - ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o/{sizeSegment=**} { - allow read: if request.auth.uid == 'mock-user'; - } - } - `, - { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/sizes/md", - file: {}, - } - ) - ).to.be.true; - }); - - it("allows only authenticated reads", async () => { - const rulesContent = ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o/{sizeSegment=**} { - allow read: if request.auth != null; - } - } - `; - - // Authenticated reads are allowed - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/sizes/md", - file: {}, - }) - ).to.be.true; - // Authenticated writes are not allowed - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.WRITE, - path: "/b/BUCKET_NAME/o/sizes/md", - file: {}, - }) - ).to.be.false; - // Unautheticated reads are not allowed - expect( - await testIfPermitted(runtime, rulesContent, { - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/sizes/md", - file: {}, - }) - ).to.be.false; - // Unautheticated writes are not allowed - expect( - await testIfPermitted(runtime, rulesContent, { - method: RulesetOperationMethod.WRITE, - path: "/b/BUCKET_NAME/o/sizes/md", - file: {}, - }) - ).to.be.false; - }); - }); - - it(".path rules are respected", async () => { - const rulesContent = ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o { - match /sizes/{size} { - allow read,write: if request.path[1] == "xl"; - } - } - }`; - - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/sizes/md", - file: {}, - }) - ).to.be.false; - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/sizes/xl", - file: {}, - }) - ).to.be.true; - }); - - it(".resource rules are respected", async () => { - const rulesContent = ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o { - match /files/{file} { - allow read, write: if request.resource.size < 5 * 1024 * 1024; - } - } - }`; - - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.WRITE, - path: "/b/BUCKET_NAME/o/files/goat", - file: { after: createFakeResourceMetadata({ size: 500 * 1024 /* 500 KiB */ }) }, - }) - ).to.be.true; - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.WRITE, - path: "/b/BUCKET_NAME/o/files/goat", - file: { after: createFakeResourceMetadata({ size: 10 * 1024 * 1024 /* 10 MiB */ }) }, - }) - ).to.be.false; - }); - }); - - describe("resource", () => { - it("should only read for small files", async () => { - const rulesContent = ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o { - match /files/{file} { - allow read: if resource.size < 5 * 1024 * 1024; - allow write: if false; - } - } - }`; - - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/files/goat", - file: { before: createFakeResourceMetadata({ size: 500 * 1024 /* 500 KiB */ }) }, - }) - ).to.be.true; - - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/files/goat", - file: { before: createFakeResourceMetadata({ size: 10 * 1024 * 1024 /* 10 MiB */ }) }, - }) - ).to.be.false; - }); - - it("should only permit upload if hash matches", async () => { - const rulesContent = ` - rules_version = '2'; - service firebase.storage { - match /b/{bucket}/o { - match /files/{file} { - allow read, write: if request.resource.md5Hash == resource.md5Hash; - } - } - }`; - const metadata1 = createFakeResourceMetadata({ md5Hash: "fake-md5-hash" }); - const metadata2 = createFakeResourceMetadata({ md5Hash: "different-md5-hash" }); - - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/files/goat", - file: { before: metadata1, after: metadata1 }, - }) - ).to.be.true; - expect( - await testIfPermitted(runtime, rulesContent, { - token: TOKENS.signedInUser, - method: RulesetOperationMethod.GET, - path: "/b/BUCKET_NAME/o/files/goat", - file: { before: metadata1, after: metadata2 }, - }) - ).to.be.false; - }); - }); -}); - -async function testIfPermitted( - runtime: StorageRulesRuntime, - rulesetContent: string, - verificationOpts: RulesetVerificationOpts, - runtimeVariableOverrides: { [s: string]: ExpressionValue } = {} -) { - const loadResult = await runtime.loadRuleset({ - files: [ - { - name: "/dev/null/storage.rules", - content: rulesetContent, - }, - ], - }); - - if (!loadResult.ruleset) { - throw new Error(JSON.stringify(loadResult.issues, undefined, 2)); - } - - const { permitted, issues } = await loadResult.ruleset.verify( - verificationOpts, - runtimeVariableOverrides - ); - - if (permitted == undefined) { - throw new Error(JSON.stringify(issues, undefined, 2)); - } - - return permitted; -} diff --git a/src/test/emulators/storage/crc.spec.ts b/src/test/emulators/storage/crc.spec.ts deleted file mode 100644 index 73114e7ac6f..00000000000 --- a/src/test/emulators/storage/crc.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { expect } from "chai"; -import { crc32c } from "../../../emulator/storage/crc"; - -/** - * Test cases adapated from: - * https://github.com/ashi009/node-fast-crc32c/blob/master/test/sets.json - */ -const stringTestCases: { - cases: { input: string; want: number }[]; -} = require("./crc-string-cases.json"); - -/** - * Test cases adapated from: - * https://github.com/ashi009/node-fast-crc32c/blob/master/test/sets.json - */ -const bufferTestCases: { - cases: { input: number[]; want: number }[]; -} = require("./crc-buffer-cases.json"); - -describe("crc", () => { - it("correctly computes crc32c from a string", () => { - const cases = stringTestCases.cases; - for (const c of cases) { - expect(crc32c(Buffer.from(c.input))).to.equal(c.want); - } - }); - - it("correctly computes crc32c from bytes", () => { - const cases = bufferTestCases.cases; - for (const c of cases) { - expect(crc32c(Buffer.from(c.input))).to.equal(c.want); - } - }); -}); diff --git a/src/test/emulators/storage/files.spec.ts b/src/test/emulators/storage/files.spec.ts deleted file mode 100644 index 511fea8e5f0..00000000000 --- a/src/test/emulators/storage/files.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from "chai"; -import { StoredFileMetadata } from "../../../emulator/storage/metadata"; -import { StorageCloudFunctions } from "../../../emulator/storage/cloudFunctions"; - -describe("files", () => { - it("can serialize and deserialize metadata", () => { - const cf = new StorageCloudFunctions("demo-project"); - const metadata = new StoredFileMetadata( - { - name: "name", - bucket: "bucket", - contentType: "mime/type", - downloadTokens: ["token123"], - customMetadata: { - foo: "bar", - }, - }, - cf, - Buffer.from("Hello, World!") - ); - - const json = StoredFileMetadata.toJSON(metadata); - const deserialized = StoredFileMetadata.fromJSON(json, cf); - expect(deserialized).to.deep.equal(metadata); - }); -}); diff --git a/src/test/ensureApiEnabled.spec.ts b/src/test/ensureApiEnabled.spec.ts deleted file mode 100644 index 2ab95c18e39..00000000000 --- a/src/test/ensureApiEnabled.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { expect } from "chai"; -import * as nock from "nock"; - -import { check, ensure, POLL_SETTINGS } from "../ensureApiEnabled"; - -const FAKE_PROJECT_ID = "my_project"; -const FAKE_API = "myapi.googleapis.com"; - -describe("ensureApiEnabled", () => { - describe("check", () => { - before(() => { - nock.disableNetConnect(); - }); - - after(() => { - nock.enableNetConnect(); - }); - - it("should call the API to check if it's enabled", async () => { - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .reply(200, { state: "ENABLED" }); - - await check(FAKE_PROJECT_ID, FAKE_API, "", true); - - expect(nock.isDone()).to.be.true; - }); - - it("should return the value from the API", async () => { - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "ENABLED" }); - - await expect(check(FAKE_PROJECT_ID, FAKE_API, "", true)).to.eventually.be.true; - - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "DISABLED" }); - - await expect(check(FAKE_PROJECT_ID, FAKE_API, "", true)).to.eventually.be.false; - }); - }); - - describe("ensure", () => { - const originalPollInterval = POLL_SETTINGS.pollInterval; - const originalPollsBeforeRetry = POLL_SETTINGS.pollsBeforeRetry; - beforeEach(() => { - nock.disableNetConnect(); - POLL_SETTINGS.pollInterval = 0; - POLL_SETTINGS.pollsBeforeRetry = 0; // Zero means "one check". - }); - - afterEach(() => { - nock.enableNetConnect(); - POLL_SETTINGS.pollInterval = originalPollInterval; - POLL_SETTINGS.pollsBeforeRetry = originalPollsBeforeRetry; - }); - - it("should verify that the API is enabled, and stop if it is", async () => { - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "ENABLED" }); - - await expect(ensure(FAKE_PROJECT_ID, FAKE_API, "", true)).to.not.be.rejected; - }); - - it("should attempt to enable the API if it is not enabled", async () => { - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "DISABLED" }); - - nock("https://serviceusage.googleapis.com") - .post(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}:enable`) - .once() - .reply(200); - - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "ENABLED" }); - - await expect(ensure(FAKE_PROJECT_ID, FAKE_API, "", true)).to.not.be.rejected; - - expect(nock.isDone()).to.be.true; - }); - - it("should retry enabling the API if it does not enable in time", async () => { - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "DISABLED" }); - - nock("https://serviceusage.googleapis.com") - .post(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}:enable`) - .twice() - .reply(200); - - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "DISABLED" }); - - nock("https://serviceusage.googleapis.com") - .get(`/v1/projects/${FAKE_PROJECT_ID}/services/${FAKE_API}`) - .once() - .reply(200, { state: "ENABLED" }); - - await expect(ensure(FAKE_PROJECT_ID, FAKE_API, "", true)).to.not.be.rejected; - - expect(nock.isDone()).to.be.true; - }); - }); -}); diff --git a/src/test/extensions/askUserForConsent.spec.ts b/src/test/extensions/askUserForConsent.spec.ts deleted file mode 100644 index 73ce8d26fd5..00000000000 --- a/src/test/extensions/askUserForConsent.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; - -import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as chai from "chai"; -chai.use(require("chai-as-promised")); -import * as sinon from "sinon"; - -import * as askUserForConsent from "../../extensions/askUserForConsent"; -import * as iam from "../../gcp/iam"; -import * as resolveSource from "../../extensions/resolveSource"; -import * as extensionHelper from "../../extensions/extensionsHelper"; - -const expect = chai.expect; - -describe("askUserForConsent", () => { - describe("formatDescription", () => { - let getRoleStub: sinon.SinonStub; - beforeEach(() => { - getRoleStub = sinon.stub(iam, "getRole"); - getRoleStub.rejects("UNDEFINED TEST BEHAVIOR"); - }); - - afterEach(() => { - getRoleStub.restore(); - }); - const roles = ["storage.objectAdmin", "datastore.viewer"]; - - it("format description correctly", () => { - const extensionName = "extension-for-test"; - const projectId = "project-for-test"; - const question = `${clc.bold( - extensionName - )} will be granted the following access to project ${clc.bold(projectId)}`; - const storageRole = { - title: "Storage Object Admin", - description: "Full control of GCS objects.", - }; - const datastoreRole = { - title: "Cloud Datastore Viewer", - description: "Read access to all Cloud Datastore resources.", - }; - const storageDescription = "- Storage Object Admin (Full control of GCS objects.)"; - const datastoreDescription = - "- Cloud Datastore Viewer (Read access to all Cloud Datastore resources.)"; - const expected = _.join([question, storageDescription, datastoreDescription], "\n"); - - getRoleStub.onFirstCall().resolves(storageRole); - getRoleStub.onSecondCall().resolves(datastoreRole); - - const actual = askUserForConsent.formatDescription(extensionName, projectId, roles); - - return expect(actual).to.eventually.deep.equal(expected); - }); - }); -}); diff --git a/src/test/extensions/askUserForParam.spec.ts b/src/test/extensions/askUserForParam.spec.ts deleted file mode 100644 index 7493719bb79..00000000000 --- a/src/test/extensions/askUserForParam.spec.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { - ask, - askForParam, - checkResponse, - getInquirerDefault, -} from "../../extensions/askUserForParam"; -import * as utils from "../../utils"; -import * as prompt from "../../prompt"; -import { ParamType } from "../../extensions/extensionsApi"; -import * as extensionsHelper from "../../extensions/extensionsHelper"; -import * as secretManagerApi from "../../gcp/secretManager"; -import * as secretsUtils from "../../extensions/secretsUtils"; - -describe("askUserForParam", () => { - const testSpec = { - param: "NAME", - type: ParamType.STRING, - label: "Name", - default: "Lauren", - validationRegex: "^[a-z,A-Z]*$", - }; - - describe("checkResponse", () => { - let logWarningSpy: sinon.SinonSpy; - beforeEach(() => { - logWarningSpy = sinon.spy(utils, "logWarning"); - }); - - afterEach(() => { - logWarningSpy.restore(); - }); - - it("should return false if required variable is not set", () => { - expect( - checkResponse("", { - param: "param", - label: "fill in the blank!", - type: ParamType.STRING, - required: true, - }) - ).to.equal(false); - expect( - logWarningSpy.calledWith(`Param param is required, but no value was provided.`) - ).to.equal(true); - }); - - it("should return false if regex validation fails", () => { - expect( - checkResponse("123", { - param: "param", - label: "fill in the blank!", - type: ParamType.STRING, - validationRegex: "foo", - required: true, - }) - ).to.equal(false); - const expectedWarning = `123 is not a valid value for param since it does not meet the requirements of the regex validation: "foo"`; - expect(logWarningSpy.calledWith(expectedWarning)).to.equal(true); - }); - - it("should return false if regex validation fails on an optional param that is not empty", () => { - expect( - checkResponse("123", { - param: "param", - label: "fill in the blank!", - type: ParamType.STRING, - validationRegex: "foo", - required: false, - }) - ).to.equal(false); - const expectedWarning = `123 is not a valid value for param since it does not meet the requirements of the regex validation: "foo"`; - expect(logWarningSpy.calledWith(expectedWarning)).to.equal(true); - }); - - it("should return true if no value is passed for an optional param", () => { - expect( - checkResponse("", { - param: "param", - label: "fill in the blank!", - type: ParamType.STRING, - validationRegex: "foo", - required: false, - }) - ).to.equal(true); - }); - - it("should use custom validation error message if provided", () => { - const message = "please enter a word with foo in it"; - expect( - checkResponse("123", { - param: "param", - label: "fill in the blank!", - type: ParamType.STRING, - validationRegex: "foo", - validationErrorMessage: message, - required: true, - }) - ).to.equal(false); - expect(logWarningSpy.calledWith(message)).to.equal(true); - }); - - it("should return true if all conditions pass", () => { - expect( - checkResponse("123", { - param: "param", - label: "fill in the blank!", - type: ParamType.STRING, - }) - ).to.equal(true); - expect(logWarningSpy.called).to.equal(false); - }); - - it("should return false if an invalid choice is selected", () => { - expect( - checkResponse("???", { - param: "param", - label: "pick one!", - type: ParamType.SELECT, - options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], - }) - ).to.equal(false); - }); - - it("should return true if an valid choice is selected", () => { - expect( - checkResponse("aaa", { - param: "param", - label: "pick one!", - type: ParamType.SELECT, - options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], - }) - ).to.equal(true); - }); - - it("should return false if multiple invalid choices are selected", () => { - expect( - checkResponse("d,e,f", { - param: "param", - label: "pick multiple!", - type: ParamType.MULTISELECT, - options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], - }) - ).to.equal(false); - }); - - it("should return true if one valid choice is selected", () => { - expect( - checkResponse("ccc", { - param: "param", - label: "pick multiple!", - type: ParamType.MULTISELECT, - options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], - }) - ).to.equal(true); - }); - - it("should return true if multiple valid choices are selected", () => { - expect( - checkResponse("aaa,bbb,ccc", { - param: "param", - label: "pick multiple!", - type: ParamType.MULTISELECT, - options: [{ value: "aaa" }, { value: "bbb" }, { value: "ccc" }], - }) - ).to.equal(true); - }); - }); - - describe("getInquirerDefaults", () => { - it("should return the label of the option whose value matches the default", () => { - const options = [ - { label: "lab", value: "val" }, - { label: "lab1", value: "val1" }, - ]; - const def = "val1"; - - const res = getInquirerDefault(options, def); - - expect(res).to.equal("lab1"); - }); - - it("should return the value of the default option if it doesnt have a label", () => { - const options = [{ label: "lab", value: "val" }, { value: "val1" }]; - const def = "val1"; - - const res = getInquirerDefault(options, def); - - expect(res).to.equal("val1"); - }); - - it("should return an empty string if a default option is not found", () => { - const options = [{ label: "lab", value: "val" }, { value: "val1" }]; - const def = "val2"; - - const res = getInquirerDefault(options, def); - - expect(res).to.equal(""); - }); - }); - describe("askForParam with string param", () => { - let promptStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - promptStub.onCall(0).returns("Invalid123"); - promptStub.onCall(1).returns("InvalidStill123"); - promptStub.onCall(2).returns("ValidName"); - }); - - afterEach(() => { - promptStub.restore(); - }); - - it("should keep prompting user until valid input is given", async () => { - await askForParam("project-id", "instance-id", testSpec, false); - expect(promptStub.calledThrice).to.be.true; - }); - }); - - describe("askForParam with secret param", () => { - const stubSecret = { - name: "new-secret", - projectId: "firebase-project-123", - }; - const stubSecretVersion = { - secret: stubSecret, - versionId: "1.0.0", - }; - const secretSpec = { - param: "API_KEY", - type: ParamType.SECRET, - label: "API Key", - default: "XXX.YYY", - }; - - let promptStub: sinon.SinonStub; - let createSecret: sinon.SinonStub; - let secretExists: sinon.SinonStub; - let addVersion: sinon.SinonStub; - let grantRole: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - promptStub.onCall(0).returns("ABC.123"); - - secretExists = sinon.stub(secretManagerApi, "secretExists"); - secretExists.onCall(0).resolves(false); - createSecret = sinon.stub(secretManagerApi, "createSecret"); - createSecret.onCall(0).resolves(stubSecret); - addVersion = sinon.stub(secretManagerApi, "addVersion"); - addVersion.onCall(0).resolves(stubSecretVersion); - - grantRole = sinon.stub(secretsUtils, "grantFirexServiceAgentSecretAdminRole"); - grantRole.onCall(0).resolves(undefined); - }); - - afterEach(() => { - promptStub.restore(); - }); - - it("should keep prompting user until valid input is given", async () => { - const result = await askForParam("project-id", "instance-id", secretSpec, false); - expect(promptStub.calledOnce).to.be.true; - expect(grantRole.calledOnce).to.be.true; - expect(result).to.be.equal( - `projects/${stubSecret.projectId}/secrets/${stubSecret.name}/versions/${stubSecretVersion.versionId}` - ); - }); - }); - - describe("ask", () => { - let subVarSpy: sinon.SinonSpy; - let promptStub: sinon.SinonStub; - - beforeEach(() => { - subVarSpy = sinon.spy(extensionsHelper, "substituteParams"); - promptStub = sinon.stub(prompt, "promptOnce"); - promptStub.returns("ValidName"); - }); - - afterEach(() => { - subVarSpy.restore(); - promptStub.restore(); - }); - - it("should call substituteParams with the right parameters", async () => { - const spec = [testSpec]; - const firebaseProjectVars = { PROJECT_ID: "my-project" }; - await ask("project-id", "instance-id", spec, firebaseProjectVars, false); - expect(subVarSpy.calledWith(spec, firebaseProjectVars)).to.be.true; - }); - }); -}); diff --git a/src/test/extensions/billingMigrationHelper.spec.ts b/src/test/extensions/billingMigrationHelper.spec.ts deleted file mode 100644 index 7bd83dc951c..00000000000 --- a/src/test/extensions/billingMigrationHelper.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as _ from "lodash"; -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../error"; -import * as nodejsMigrationHelper from "../../extensions/billingMigrationHelper"; -import * as prompt from "../../prompt"; - -const NO_RUNTIME_SPEC = { - name: "test", - specVersion: "v1beta", - displayName: "Old", - description: "descriptive", - version: "1.0.0", - license: "MIT", - resources: [ - { - name: "resource1", - type: "firebaseextensions.v1beta.function", - description: "desc", - properties: {}, - }, - ], - author: { authorName: "Tester" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [], -}; - -const NODE8_SPEC = { - name: "test", - specVersion: "v1beta", - displayName: "Old", - description: "descriptive", - version: "1.0.0", - license: "MIT", - resources: [ - { - name: "resource1", - type: "firebaseextensions.v1beta.function", - description: "desc", - properties: { runtime: "nodejs8" }, - }, - ], - author: { authorName: "Tester" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [], -}; - -const NODE10_SPEC = { - name: "test", - specVersion: "v1beta", - displayName: "Old", - description: "descriptive", - version: "1.0.0", - license: "MIT", - resources: [ - { - name: "resource1", - type: "firebaseextensions.v1beta.function", - description: "desc", - properties: { runtime: "nodejs10" }, - }, - ], - author: { authorName: "Tester" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [], -}; - -describe("billingMigrationHelper", () => { - let promptStub: sinon.SinonStub; - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - }); - - afterEach(() => { - promptStub.restore(); - }); - - describe("displayCreateBillingNotice", () => { - it("should notify the user if the runtime requires nodejs10", () => { - promptStub.resolves(true); - const newSpec = _.cloneDeep(NODE10_SPEC); - - expect(nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true)).not.to.be - .rejected; - expect(promptStub.callCount).to.equal(1); - }); - - it("should notify the user if the runtime does not require nodejs (explicit)", () => { - promptStub.resolves(true); - const newSpec = _.cloneDeep(NODE8_SPEC); - - expect(nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true)).not.to.be - .rejected; - expect(promptStub.callCount).to.equal(0); - }); - - it("should notify the user if the runtime does not require nodejs (implicit)", () => { - promptStub.resolves(true); - const newSpec = _.cloneDeep(NO_RUNTIME_SPEC); - - expect(nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true)).not.to.be - .rejected; - expect(promptStub.callCount).to.equal(0); - }); - - it("should error if the user doesn't give consent", () => { - promptStub.resolves(false); - const newSpec = _.cloneDeep(NODE10_SPEC); - - expect( - nodejsMigrationHelper.displayNode10CreateBillingNotice(newSpec, true) - ).to.be.rejectedWith(FirebaseError, "Cancelled"); - }); - }); -}); diff --git a/src/test/extensions/changelog.spec.ts b/src/test/extensions/changelog.spec.ts deleted file mode 100644 index a2879547efe..00000000000 --- a/src/test/extensions/changelog.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import * as chai from "chai"; -import { expect } from "chai"; -chai.use(require("chai-as-promised")); -import * as sinon from "sinon"; - -import * as changelog from "../../extensions/changelog"; -import * as extensionApi from "../../extensions/extensionsApi"; - -function testExtensionVersion( - version: string, - releaseNotes?: string -): extensionApi.ExtensionVersion { - return { - name: `publishers/test/extensions/test/versions/${version}`, - ref: `test/test@${version}`, - state: "PUBLISHED", - hash: "abc123", - sourceDownloadUri: "https://google.com", - releaseNotes, - spec: { - name: "test", - version, - resources: [], - params: [], - sourceUrl: "https://google.com", - }, - }; -} - -describe("changelog", () => { - describe("GetReleaseNotesForUpdate", () => { - let listExtensionVersionStub: sinon.SinonStub; - - beforeEach(() => { - listExtensionVersionStub = sinon.stub(extensionApi, "listExtensionVersions"); - }); - - afterEach(() => { - listExtensionVersionStub.restore(); - }); - - it("should return release notes for each version in the update", async () => { - const extensionVersions: extensionApi.ExtensionVersion[] = [ - testExtensionVersion("0.1.1", "foo"), - testExtensionVersion("0.1.2", "bar"), - ]; - listExtensionVersionStub - .withArgs("test/test", `id<="0.1.2" AND id>"0.1.0"`) - .returns(extensionVersions); - const want = { - "0.1.1": "foo", - "0.1.2": "bar", - }; - - const got = await changelog.getReleaseNotesForUpdate({ - extensionRef: "test/test", - fromVersion: "0.1.0", - toVersion: "0.1.2", - }); - - expect(got).to.deep.equal(want); - }); - - it("should exclude versions that don't have releaseNotes", async () => { - const extensionVersions: extensionApi.ExtensionVersion[] = [ - testExtensionVersion("0.1.1", "foo"), - testExtensionVersion("0.1.2"), - ]; - listExtensionVersionStub - .withArgs("test/test", `id<="0.1.2" AND id>"0.1.0"`) - .resolves(extensionVersions); - const want = { - "0.1.1": "foo", - }; - - const got = await changelog.getReleaseNotesForUpdate({ - extensionRef: "test/test", - fromVersion: "0.1.0", - toVersion: "0.1.2", - }); - - expect(got).to.deep.equal(want); - }); - }); - - describe("breakingChangesInUpdate", () => { - const testCases: { - description: string; - in: string[]; - want: string[]; - }[] = [ - { - description: "should return no breaking changes", - in: ["0.1.0", "0.1.1", "0.1.2"], - want: [], - }, - { - description: "should return prerelease breaking change", - in: ["0.1.0", "0.1.1", "0.2.0"], - want: ["0.2.0"], - }, - { - description: "should return breaking change", - in: ["1.1.0", "1.1.1", "2.0.0"], - want: ["2.0.0"], - }, - { - description: "should return multiple breaking changes", - in: ["0.1.0", "0.2.1", "1.0.0"], - want: ["0.2.1", "1.0.0"], - }, - ]; - for (const testCase of testCases) { - it(testCase.description, () => { - const got = changelog.breakingChangesInUpdate(testCase.in); - - expect(got).to.deep.equal(testCase.want); - }); - } - }); - - describe("parseChangelog", () => { - const testCases: { - description: string; - in: string; - want: Record; - }[] = [ - { - description: "should split changelog by version", - in: "## Version 0.1.0\nNotes\n## Version 0.1.1\nNew notes", - want: { - "0.1.0": "Notes", - "0.1.1": "New notes", - }, - }, - { - description: "should ignore text not in a version", - in: "Some random words\n## Version 0.1.0\nNotes\n## Version 0.1.1\nNew notes", - want: { - "0.1.0": "Notes", - "0.1.1": "New notes", - }, - }, - ]; - for (const testCase of testCases) { - it(testCase.description, () => { - const got = changelog.parseChangelog(testCase.in); - - expect(got).to.deep.equal(testCase.want); - }); - } - }); -}); diff --git a/src/test/extensions/displayExtensionInfo.spec.ts b/src/test/extensions/displayExtensionInfo.spec.ts deleted file mode 100644 index 52a4e61a63c..00000000000 --- a/src/test/extensions/displayExtensionInfo.spec.ts +++ /dev/null @@ -1,257 +0,0 @@ -import * as _ from "lodash"; -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../error"; -import * as displayExtensionInfo from "../../extensions/displayExtensionInfo"; -import * as prompt from "../../prompt"; - -const SPEC = { - name: "test", - displayName: "Old", - description: "descriptive", - version: "0.1.0", - license: "MIT", - apis: [ - { apiName: "api1", reason: "" }, - { apiName: "api2", reason: "" }, - ], - roles: [ - { role: "role1", reason: "" }, - { role: "role2", reason: "" }, - ], - resources: [ - { name: "resource1", type: "firebaseextensions.v1beta.function", description: "desc" }, - { name: "resource2", type: "other", description: "" }, - ], - author: { authorName: "Tester", url: "firebase.google.com" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [], -}; - -describe("displayExtensionInfo", () => { - describe("displayExtInfo", () => { - it("should display info during install", () => { - const loggedLines = displayExtensionInfo.displayExtInfo(SPEC.name, "", SPEC); - const expected: string[] = ["**Name**: Old", "**Description**: descriptive"]; - expect(loggedLines).to.eql(expected); - }); - it("should display additional information for a published extension", () => { - const loggedLines = displayExtensionInfo.displayExtInfo( - SPEC.name, - "testpublisher", - SPEC, - true - ); - const expected: string[] = [ - "**Name**: Old", - "**Publisher**: testpublisher", - "**Description**: descriptive", - "**License**: MIT", - "**Source code**: test.com", - ]; - expect(loggedLines).to.eql(expected); - }); - }); - describe("displayUpdateChangesNoInput", () => { - it("should display changes to display name", () => { - const newSpec = _.cloneDeep(SPEC); - newSpec.displayName = "new"; - - const loggedLines = displayExtensionInfo.displayUpdateChangesNoInput(SPEC, newSpec); - - const expected = ["", "**Name:**", "\u001b[31m- Old\u001b[39m", "\u001b[32m+ new\u001b[39m"]; - expect(loggedLines).to.include.members(expected); - }); - - it("should display changes to description", () => { - const newSpec = _.cloneDeep(SPEC); - newSpec.description = "even better"; - - const loggedLines = displayExtensionInfo.displayUpdateChangesNoInput(SPEC, newSpec); - - const expected = [ - "", - "**Description:**", - "\u001b[31m- descriptive\u001b[39m", - "\u001b[32m+ even better\u001b[39m", - ]; - expect(loggedLines).to.include.members(expected); - }); - - it("should notify the user if billing is no longer required", () => { - const newSpec = _.cloneDeep(SPEC); - newSpec.billingRequired = false; - - const loggedLines = displayExtensionInfo.displayUpdateChangesNoInput(SPEC, newSpec); - - const expected = ["", "**Billing is no longer required for this extension.**"]; - expect(loggedLines).to.include.members(expected); - }); - }); - - describe("displayUpdateChangesRequiringConfirmation", () => { - let promptStub: sinon.SinonStub; - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - }); - - afterEach(() => { - promptStub.restore(); - }); - - it("should prompt for changes to license and continue if user gives consent", () => { - promptStub.resolves(true); - const newSpec = _.cloneDeep(SPEC); - newSpec.license = "To Kill"; - - expect( - displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: SPEC, - newSpec, - nonInteractive: false, - force: false, - }) - ).not.to.be.rejected; - - expect(promptStub.callCount).to.equal(1); - }); - - it("should prompt for changes to apis and continue if user gives consent", () => { - promptStub.resolves(true); - const newSpec = _.cloneDeep(SPEC); - newSpec.apis = [ - { apiName: "api2", reason: "" }, - { apiName: "api3", reason: "" }, - ]; - - expect( - displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: SPEC, - newSpec, - nonInteractive: false, - force: false, - }) - ).not.to.be.rejected; - - expect(promptStub.callCount).to.equal(1); - }); - - it("should prompt for changes to roles and continue if user gives consent", () => { - promptStub.resolves(true); - const newSpec = _.cloneDeep(SPEC); - newSpec.roles = [ - { role: "role2", reason: "" }, - { role: "role3", reason: "" }, - ]; - - expect( - displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: SPEC, - newSpec, - nonInteractive: false, - force: false, - }) - ).not.to.be.rejected; - - expect(promptStub.callCount).to.equal(1); - }); - - it("should prompt for changes to resources and continue if user gives consent", () => { - promptStub.resolves(true); - const newSpec = _.cloneDeep(SPEC); - newSpec.resources = [ - { name: "resource3", type: "firebaseextensions.v1beta.function", description: "new desc" }, - { name: "resource2", type: "other", description: "" }, - ]; - - expect( - displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: SPEC, - newSpec, - nonInteractive: false, - force: false, - }) - ).not.to.be.rejected; - - expect(promptStub.callCount).to.equal(1); - }); - - it("should prompt for changes to resources and continue if user gives consent", () => { - promptStub.resolves(true); - const oldSpec = _.cloneDeep(SPEC); - oldSpec.billingRequired = false; - - expect( - displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: oldSpec, - newSpec: SPEC, - nonInteractive: false, - force: false, - }) - ).not.to.be.rejected; - - expect(promptStub.callCount).to.equal(1); - }); - - it("should exit if the user consents to one change but rejects another", () => { - promptStub.resolves(true); - promptStub.resolves(false); - const newSpec = _.cloneDeep(SPEC); - newSpec.license = "New"; - newSpec.roles = [ - { role: "role2", reason: "" }, - { role: "role3", reason: "" }, - ]; - - expect( - displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: SPEC, - newSpec, - nonInteractive: false, - force: false, - }) - ).to.be.rejectedWith( - FirebaseError, - "Unable to update this extension instance without explicit consent for the change to 'License'" - ); - - expect(promptStub.callCount).to.equal(1); - }); - - it("should error if the user doesn't give consent", () => { - promptStub.resolves(false); - const newSpec = _.cloneDeep(SPEC); - newSpec.license = "new"; - - expect( - displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: SPEC, - newSpec, - nonInteractive: false, - force: false, - }) - ).to.be.rejectedWith( - FirebaseError, - "Unable to update this extension instance without explicit consent for the change to 'License'." - ); - }); - - it("shouldn't prompt the user if no changes require confirmation", async () => { - promptStub.resolves(false); - const newSpec = _.cloneDeep(SPEC); - newSpec.version = "1.1.0"; - - await displayExtensionInfo.displayUpdateChangesRequiringConfirmation({ - spec: SPEC, - newSpec, - nonInteractive: false, - force: false, - }); - - expect(promptStub).not.to.have.been.called; - }); - }); -}); diff --git a/src/test/extensions/emulator/optionsHelper.spec.ts b/src/test/extensions/emulator/optionsHelper.spec.ts deleted file mode 100644 index f470f310d38..00000000000 --- a/src/test/extensions/emulator/optionsHelper.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as optionsHelper from "../../../extensions/emulator/optionsHelper"; -import { ExtensionSpec } from "../../../extensions/extensionsApi"; -import * as paramHelper from "../../../extensions/paramHelper"; - -describe("optionsHelper", () => { - describe("getParams", () => { - const testOptions = { - project: "test", - testParams: "test.env", - }; - const autoParams = { - PROJECT_ID: "test", - EXT_INSTANCE_ID: "test", - DATABASE_INSTANCE: "test", - DATABASE_URL: "https://test.firebaseio.com", - STORAGE_BUCKET: "test.appspot.com", - }; - let testSpec: ExtensionSpec; - let readEnvFileStub: sinon.SinonStub; - - beforeEach(() => { - testSpec = { - name: "test", - version: "0.1.0", - resources: [], - sourceUrl: "https://my.stuff.com", - params: [], - }; - readEnvFileStub = sinon.stub(paramHelper, "readEnvFile"); - }); - - afterEach(() => { - readEnvFileStub.restore(); - }); - - it("should return user and autopopulated params", () => { - testSpec.params = [ - { - label: "param1", - param: "USER_PARAM1", - }, - { - label: "param2", - param: "USER_PARAM2", - }, - ]; - readEnvFileStub.returns({ - USER_PARAM1: "val1", - USER_PARAM2: "val2", - }); - - expect(optionsHelper.getParams(testOptions, testSpec)).to.deep.eq({ - ...{ - USER_PARAM1: "val1", - USER_PARAM2: "val2", - }, - ...autoParams, - }); - }); - - it("should subsitute into params that reference other params", () => { - testSpec.params = [ - { - label: "param1", - param: "USER_PARAM1", - }, - { - label: "param2", - param: "USER_PARAM2", - }, - { - label: "param3", - param: "USER_PARAM3", - }, - ]; - readEnvFileStub.returns({ - USER_PARAM1: "${PROJECT_ID}-hello", - USER_PARAM2: "val2", - USER_PARAM3: "${USER_PARAM2}", - }); - - expect(optionsHelper.getParams(testOptions, testSpec)).to.deep.eq({ - ...{ - USER_PARAM1: "test-hello", - USER_PARAM2: "val2", - USER_PARAM3: "val2", - }, - ...autoParams, - }); - }); - - it("should fallback to defaults if a value isn't provided", () => { - testSpec.params = [ - { - label: "param1", - param: "USER_PARAM1", - default: "hi", - required: true, - }, - { - label: "param2", - param: "USER_PARAM2", - default: "hello", - required: true, - }, - ]; - readEnvFileStub.returns({}); - - expect(optionsHelper.getParams(testOptions, testSpec)).to.deep.eq({ - ...{ - USER_PARAM1: "hi", - USER_PARAM2: "hello", - }, - ...autoParams, - }); - }); - }); -}); diff --git a/src/test/extensions/emulator/triggerHelper.spec.ts b/src/test/extensions/emulator/triggerHelper.spec.ts deleted file mode 100644 index 509a3e8ac58..00000000000 --- a/src/test/extensions/emulator/triggerHelper.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { expect } from "chai"; -import * as triggerHelper from "../../../extensions/emulator/triggerHelper"; - -describe("triggerHelper", () => { - describe("functionResourceToEmulatedTriggerDefintion", () => { - it("should assign valid properties from the resource to the ETD and ignore others", () => { - const testResource = { - name: "test-resource", - entryPoint: "functionName", - properties: { - timeout: "3s", - location: "us-east1", - availableMemoryMb: 1024, - somethingInvalid: "a value", - }, - }; - const expected = { - platform: "gcfv1", - availableMemoryMb: 1024, - entryPoint: "test-resource", - name: "test-resource", - regions: ["us-east1"], - timeout: "3s", - }; - - const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); - - expect(result).to.eql(expected); - }); - - it("should handle HTTPS triggers", () => { - const testResource = { - name: "test-resource", - entryPoint: "functionName", - properties: { - httpsTrigger: {}, - }, - }; - const expected = { - platform: "gcfv1", - entryPoint: "test-resource", - name: "test-resource", - httpsTrigger: {}, - }; - - const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); - - expect(result).to.eql(expected); - }); - - it("should handle firestore triggers", () => { - const testResource = { - name: "test-resource", - entryPoint: "functionName", - properties: { - eventTrigger: { - eventType: "providers/cloud.firestore/eventTypes/document.write", - resource: "myResource", - }, - }, - }; - const expected = { - platform: "gcfv1", - entryPoint: "test-resource", - name: "test-resource", - eventTrigger: { - service: "firestore.googleapis.com", - resource: "myResource", - eventType: "providers/cloud.firestore/eventTypes/document.write", - }, - }; - - const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); - - expect(result).to.eql(expected); - }); - - it("should handle database triggers", () => { - const testResource = { - name: "test-resource", - entryPoint: "functionName", - properties: { - eventTrigger: { - eventType: "providers/google.firebase.database/eventTypes/ref.create", - resource: "myResource", - }, - }, - }; - const expected = { - platform: "gcfv1", - entryPoint: "test-resource", - name: "test-resource", - eventTrigger: { - eventType: "providers/google.firebase.database/eventTypes/ref.create", - service: "firebaseio.com", - resource: "myResource", - }, - }; - - const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); - - expect(result).to.eql(expected); - }); - - it("should handle pubsub triggers", () => { - const testResource = { - name: "test-resource", - entryPoint: "functionName", - properties: { - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "myResource", - }, - }, - }; - const expected = { - platform: "gcfv1", - entryPoint: "test-resource", - name: "test-resource", - eventTrigger: { - service: "pubsub.googleapis.com", - resource: "myResource", - eventType: "google.pubsub.topic.publish", - }, - }; - - const result = triggerHelper.functionResourceToEmulatedTriggerDefintion(testResource); - - expect(result).to.eql(expected); - }); - }); -}); diff --git a/src/test/extensions/extensionsApi.spec.ts b/src/test/extensions/extensionsApi.spec.ts deleted file mode 100644 index cbfedd60a24..00000000000 --- a/src/test/extensions/extensionsApi.spec.ts +++ /dev/null @@ -1,1057 +0,0 @@ -import * as _ from "lodash"; -import { expect } from "chai"; -import * as nock from "nock"; - -import * as api from "../../api"; -import { FirebaseError } from "../../error"; -import * as extensionsApi from "../../extensions/extensionsApi"; -import * as refs from "../../extensions/refs"; - -const VERSION = "v1beta"; -const PROJECT_ID = "test-project"; -const INSTANCE_ID = "test-extensions-instance"; -const PUBLISHER_ID = "test-project"; -const EXTENSION_ID = "test-extension"; -const EXTENSION_VERSION = "0.0.1"; - -const EXT_SPEC = { - name: "cool-things", - version: "1.0.0", - resources: { - name: "cool-resource", - type: "firebaseextensions.v1beta.function", - }, - sourceUrl: "www.google.com/cool-things-here", -}; -const TEST_EXTENSION_1 = { - name: "publishers/test-pub/extensions/ext-one", - ref: "test-pub/ext-one", - state: "PUBLISHED", - createTime: "2020-06-30T00:21:06.722782Z", -}; -const TEST_EXTENSION_2 = { - name: "publishers/test-pub/extensions/ext-two", - ref: "test-pub/ext-two", - state: "PUBLISHED", - createTime: "2020-06-30T00:21:06.722782Z", -}; -const TEST_EXTENSION_3 = { - name: "publishers/test-pub/extensions/ext-three", - ref: "test-pub/ext-three", - state: "UNPUBLISHED", - createTime: "2020-06-30T00:21:06.722782Z", -}; -const TEST_EXT_VERSION_1 = { - name: "publishers/test-pub/extensions/ext-one/versions/0.0.1", - ref: "test-pub/ext-one@0.0.1", - spec: EXT_SPEC, - state: "UNPUBLISHED", - hash: "12345", - createTime: "2020-06-30T00:21:06.722782Z", -}; -const TEST_EXT_VERSION_2 = { - name: "publishers/test-pub/extensions/ext-one/versions/0.0.2", - ref: "test-pub/ext-one@0.0.2", - spec: EXT_SPEC, - state: "PUBLISHED", - hash: "23456", - createTime: "2020-06-30T00:21:06.722782Z", -}; -const TEST_EXT_VERSION_3 = { - name: "publishers/test-pub/extensions/ext-one/versions/0.0.3", - ref: "test-pub/ext-one@0.0.3", - spec: EXT_SPEC, - state: "PUBLISHED", - hash: "34567", - createTime: "2020-06-30T00:21:06.722782Z", -}; -const TEST_EXT_VERSION_4 = { - name: "publishers/test-pub/extensions/ext-one/versions/0.0.4", - ref: "test-pub/ext-one@0.0.4", - spec: EXT_SPEC, - state: "DEPRECATED", - hash: "34567", - createTime: "2020-06-30T00:21:06.722782Z", - deprecationMessage: "This version is deprecated", -}; -const TEST_INSTANCE_1 = { - name: "projects/invader-zim/instances/image-resizer-1", - createTime: "2019-06-19T00:20:10.416947Z", - updateTime: "2019-06-19T00:21:06.722782Z", - state: "ACTIVE", - config: { - name: - "projects/invader-zim/instances/image-resizer-1/configurations/5b1fb749-764d-4bd1-af60-bb7f22d27860", - createTime: "2019-06-19T00:21:06.722782Z", - }, -}; - -const TEST_INSTANCE_2 = { - name: "projects/invader-zim/instances/image-resizer", - createTime: "2019-05-19T00:20:10.416947Z", - updateTime: "2019-05-19T00:20:10.416947Z", - state: "ACTIVE", - config: { - name: - "projects/invader-zim/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", - createTime: "2019-05-19T00:20:10.416947Z", - }, -}; - -const TEST_INSTANCES_RESPONSE = { - instances: [TEST_INSTANCE_1, TEST_INSTANCE_2], -}; - -const TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN: any = _.cloneDeep(TEST_INSTANCES_RESPONSE); -TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN.nextPageToken = "abc123"; - -const PACKAGE_URI = "https://storage.googleapis.com/ABCD.zip"; -const SOURCE_NAME = "projects/firebasemods/sources/abcd"; -const TEST_SOURCE = { - name: SOURCE_NAME, - packageUri: PACKAGE_URI, - hash: "deadbeef", - spec: { - name: "test", - displayName: "Old", - description: "descriptive", - version: "1.0.0", - license: "MIT", - resources: [ - { - name: "resource1", - type: "firebaseextensions.v1beta.function", - description: "desc", - propertiesYaml: - "eventTrigger:\n eventType: providers/cloud.firestore/eventTypes/document.write\n resource: projects/${PROJECT_ID}/databases/(default)/documents/${COLLECTION_PATH}/{documentId}\nlocation: ${LOCATION}", - }, - ], - author: { authorName: "Tester" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [], - }, -}; - -const NEXT_PAGE_TOKEN = "random123"; -const PUBLISHED_EXTENSIONS = { extensions: [TEST_EXTENSION_1, TEST_EXTENSION_2] }; -const ALL_EXTENSIONS = { - extensions: [TEST_EXTENSION_1, TEST_EXTENSION_2, TEST_EXTENSION_3], -}; -const PUBLISHED_WITH_TOKEN = { extensions: [TEST_EXTENSION_1], nextPageToken: NEXT_PAGE_TOKEN }; -const NEXT_PAGE_EXTENSIONS = { extensions: [TEST_EXTENSION_2] }; - -const PUBLISHED_EXT_VERSIONS = { extensionVersions: [TEST_EXT_VERSION_2, TEST_EXT_VERSION_3] }; -const ALL_EXT_VERSIONS = { - extensionVersions: [TEST_EXT_VERSION_1, TEST_EXT_VERSION_2, TEST_EXT_VERSION_3], -}; -const PUBLISHED_VERSIONS_WITH_TOKEN = { - extensionVersions: [TEST_EXT_VERSION_2], - nextPageToken: NEXT_PAGE_TOKEN, -}; -const NEXT_PAGE_VERSIONS = { extensionVersions: [TEST_EXT_VERSION_3] }; - -describe("extensions", () => { - describe("listInstances", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should return a list of installed extensions instances", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) - .query((queryParams: any) => { - return queryParams.pageSize === "100"; - }) - .reply(200, TEST_INSTANCES_RESPONSE); - - const instances = await extensionsApi.listInstances(PROJECT_ID); - - expect(instances).to.deep.equal(TEST_INSTANCES_RESPONSE.instances); - expect(nock.isDone()).to.be.true; - }); - - it("should query for more installed extensions if the response has a next_page_token", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) - .query((queryParams: any) => { - return queryParams.pageSize === "100"; - }) - .reply(200, TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN); - nock(api.extensionsOrigin) - .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) - .query((queryParams: any) => { - return queryParams.pageToken === "abc123"; - }) - .reply(200, TEST_INSTANCES_RESPONSE); - - const instances = await extensionsApi.listInstances(PROJECT_ID); - - const expected = TEST_INSTANCES_RESPONSE.instances.concat( - TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN.instances - ); - expect(instances).to.deep.equal(expected); - expect(nock.isDone()).to.be.true; - }); - - it("should throw FirebaseError if any call returns an error", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) - .query((queryParams: any) => { - return queryParams.pageSize === "100"; - }) - .reply(200, TEST_INSTANCES_RESPONSE_NEXT_PAGE_TOKEN); - nock(api.extensionsOrigin) - .get(`/${VERSION}/projects/${PROJECT_ID}/instances`) - .query((queryParams: any) => { - return queryParams.pageToken === "abc123"; - }) - .reply(503); - - await expect(extensionsApi.listInstances(PROJECT_ID)).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("createInstance", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a POST call to the correct endpoint, and then poll on the returned operation when given a source", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) - .query({ validateOnly: "false" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.createInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionSource: { - state: "ACTIVE", - name: "sources/blah", - packageUri: "https://test.fake/pacakge.zip", - hash: "abc123", - spec: { name: "", version: "0.1.0", sourceUrl: "", roles: [], resources: [], params: [] }, - }, - params: {}, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a POST call to the correct endpoint, and then poll on the returned operation when given an Extension ref", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) - .query({ validateOnly: "false" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.createInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionVersionRef: "test-pub/test-ext@0.1.0", - params: {}, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a POST and not poll if validateOnly=true", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) - .query({ validateOnly: "true" }) - .reply(200, { name: "operations/abc123", done: true }); - - await extensionsApi.createInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionVersionRef: "test-pub/test-ext@0.1.0", - params: {}, - validateOnly: true, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if create returns an error response", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/instances/`) - .query({ validateOnly: "false" }) - .reply(500); - - await expect( - extensionsApi.createInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionSource: { - state: "ACTIVE", - name: "sources/blah", - packageUri: "https://test.fake/pacakge.zip", - hash: "abc123", - spec: { - name: "", - version: "0.1.0", - sourceUrl: "", - roles: [], - resources: [], - params: [], - }, - }, - params: {}, - }) - ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500, Unknown Error"); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("configureInstance", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a PATCH call to the correct endpoint, and then poll on the returned operation", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.params", validateOnly: "false" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin) - .get(`/${VERSION}/operations/abc123`) - .reply(200, { done: false }) - .get(`/${VERSION}/operations/abc123`) - .reply(200, { done: true }); - - await extensionsApi.configureInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - params: { MY_PARAM: "value" }, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a PATCH and not poll if validateOnly=true", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.params", validateOnly: "true" }) - .reply(200, { name: "operations/abc123", done: true }); - - await extensionsApi.configureInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - params: { MY_PARAM: "value" }, - validateOnly: true, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if update returns an error response", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.params", validateOnly: false }) - .reply(500); - - await expect( - extensionsApi.configureInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - params: { MY_PARAM: "value" }, - }) - ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500"); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("deleteInstance", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a DELETE call to the correct endpoint, and then poll on the returned operation", async () => { - nock(api.extensionsOrigin) - .delete(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.deleteInstance(PROJECT_ID, INSTANCE_ID); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if delete returns an error response", async () => { - nock(api.extensionsOrigin) - .delete(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .reply(404); - - await expect(extensionsApi.deleteInstance(PROJECT_ID, INSTANCE_ID)).to.be.rejectedWith( - FirebaseError - ); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("updateInstance", () => { - const testSource: extensionsApi.ExtensionSource = { - state: "ACTIVE", - name: "abc123", - packageUri: "www.google.com/pack.zip", - hash: "abc123", - spec: { - name: "abc123", - version: "0.1.0", - resources: [], - params: [], - sourceUrl: "www.google.com/pack.zip", - }, - }; - afterEach(() => { - nock.cleanAll(); - }); - - it("should include config.param in updateMask is params are changed", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.source.name,config.params", validateOnly: "false" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.updateInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionSource: testSource, - params: { - MY_PARAM: "value", - }, - }); - - expect(nock.isDone()).to.be.true; - }); - - it("should not include config.param in updateMask is params aren't changed", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.source.name", validateOnly: "false" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.updateInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionSource: testSource, - }); - - expect(nock.isDone()).to.be.true; - }); - - it("should make a PATCH and not poll if validateOnly=true", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.source.name", validateOnly: "true" }) - .reply(200, { name: "operations/abc123", done: true }); - - await extensionsApi.updateInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionSource: testSource, - validateOnly: true, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should make a PATCH and not poll if validateOnly=true", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.source.name", validateOnly: "true" }) - .reply(200, { name: "operations/abc123", done: true }); - - await extensionsApi.updateInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionSource: testSource, - validateOnly: true, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if update returns an error response", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.source.name,config.params", validateOnly: false }) - .reply(500); - - await expect( - extensionsApi.updateInstance({ - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - extensionSource: testSource, - params: { - MY_PARAM: "value", - }, - }) - ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500"); - - expect(nock.isDone()).to.be.true; - }); - }); - - describe("getInstance", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a GET call to the correct endpoint", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .reply(200); - - await extensionsApi.getInstance(PROJECT_ID, INSTANCE_ID); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .reply(404); - - await expect(extensionsApi.getInstance(PROJECT_ID, INSTANCE_ID)).to.be.rejectedWith( - FirebaseError - ); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("getSource", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a GET call to the correct endpoint", async () => { - nock(api.extensionsOrigin).get(`/${VERSION}/${SOURCE_NAME}`).reply(200, TEST_SOURCE); - - const source = await extensionsApi.getSource(SOURCE_NAME); - expect(nock.isDone()).to.be.true; - expect(source.spec.resources).to.have.lengthOf(1); - expect(source.spec.resources[0]).to.have.property("properties"); - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - nock(api.extensionsOrigin).get(`/${VERSION}/${SOURCE_NAME}`).reply(404); - - await expect(extensionsApi.getSource(SOURCE_NAME)).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("createSource", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a POST call to the correct endpoint, and then poll on the returned operation", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/sources/`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin) - .get(`/${VERSION}/operations/abc123`) - .reply(200, { done: true, response: TEST_SOURCE }); - - const source = await extensionsApi.createSource(PROJECT_ID, PACKAGE_URI, ",./"); - expect(nock.isDone()).to.be.true; - expect(source.spec.resources).to.have.lengthOf(1); - expect(source.spec.resources[0]).to.have.property("properties"); - }); - - it("should throw a FirebaseError if create returns an error response", async () => { - nock(api.extensionsOrigin).post(`/${VERSION}/projects/${PROJECT_ID}/sources/`).reply(500); - - await expect(extensionsApi.createSource(PROJECT_ID, PACKAGE_URI, "./")).to.be.rejectedWith( - FirebaseError, - "HTTP Error: 500, Unknown Error" - ); - expect(nock.isDone()).to.be.true; - }); - - it("stop polling and throw if the operation call throws an unexpected error", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/sources/`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(502, {}); - - await expect(extensionsApi.createSource(PROJECT_ID, PACKAGE_URI, "./")).to.be.rejectedWith( - FirebaseError, - "HTTP Error: 502, Unknown Error" - ); - expect(nock.isDone()).to.be.true; - }); - }); -}); - -describe("publishExtensionVersion", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a POST call to the correct endpoint, and then poll on the returned operation", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/publishers/test-pub/extensions/ext-one/versions:publish`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { - done: true, - response: TEST_EXT_VERSION_3, - }); - - const res = await extensionsApi.publishExtensionVersion( - TEST_EXT_VERSION_3.ref, - "www.google.com/test-extension.zip" - ); - expect(res).to.deep.equal(TEST_EXT_VERSION_3); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if publishExtensionVersion returns an error response", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions:publish`) - .reply(500); - - await expect( - extensionsApi.publishExtensionVersion( - `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, - "www.google.com/test-extension.zip", - "/" - ) - ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500, Unknown Error"); - expect(nock.isDone()).to.be.true; - }); - - it("stop polling and throw if the operation call throws an unexpected error", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions:publish`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(502, {}); - - await expect( - extensionsApi.publishExtensionVersion( - `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`, - "www.google.com/test-extension.zip", - "/" - ) - ).to.be.rejectedWith(FirebaseError, "HTTP Error: 502, Unknown Error"); - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error for an invalid ref", async () => { - await expect( - extensionsApi.publishExtensionVersion( - `${PUBLISHER_ID}/${EXTENSION_ID}`, - "www.google.com/test-extension.zip", - "/" - ) - ).to.be.rejectedWith(FirebaseError, "ExtensionVersion ref"); - }); -}); - -describe("deleteExtension", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a DELETE call to the correct endpoint", async () => { - nock(api.extensionsOrigin) - .delete(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) - .reply(200); - - await extensionsApi.deleteExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - nock(api.extensionsOrigin) - .delete(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) - .reply(404); - - await expect( - extensionsApi.deleteExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`) - ).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error for an invalid ref", async () => { - await expect( - extensionsApi.deleteExtension(`${PUBLISHER_ID}/${EXTENSION_ID}@0.1.0`) - ).to.be.rejectedWith(FirebaseError, "must not contain a version"); - }); -}); - -describe("unpublishExtension", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a POST call to the correct endpoint", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}:unpublish`) - .reply(200); - - await extensionsApi.unpublishExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}:unpublish`) - .reply(404); - - await expect( - extensionsApi.unpublishExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`) - ).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error for an invalid ref", async () => { - await expect( - extensionsApi.unpublishExtension(`${PUBLISHER_ID}/${EXTENSION_ID}@0.1.0`) - ).to.be.rejectedWith(FirebaseError, "must not contain a version"); - }); -}); - -describe("getExtension", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a GET call to the correct endpoint", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) - .reply(200); - - await extensionsApi.getExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}`) - .reply(404); - - await expect(extensionsApi.getExtension(`${PUBLISHER_ID}/${EXTENSION_ID}`)).to.be.rejectedWith( - FirebaseError - ); - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error for an invalid ref", async () => { - await expect(extensionsApi.getExtension(`${PUBLISHER_ID}`)).to.be.rejectedWith( - FirebaseError, - "Unable to parse" - ); - }); -}); - -describe("getExtensionVersion", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a GET call to the correct endpoint", async () => { - nock(api.extensionsOrigin) - .get( - `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions/${EXTENSION_VERSION}` - ) - .reply(200, TEST_EXTENSION_1); - - const got = await extensionsApi.getExtensionVersion( - `${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}` - ); - expect(got).to.deep.equal(TEST_EXTENSION_1); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - nock(api.extensionsOrigin) - .get( - `/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions/${EXTENSION_VERSION}` - ) - .reply(404); - - await expect( - extensionsApi.getExtensionVersion(`${PUBLISHER_ID}/${EXTENSION_ID}@${EXTENSION_VERSION}`) - ).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error for an invalid ref", async () => { - await expect( - extensionsApi.getExtensionVersion(`${PUBLISHER_ID}//${EXTENSION_ID}`) - ).to.be.rejectedWith(FirebaseError, "Unable to parse"); - }); -}); - -describe("listExtensions", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should return a list of published extensions", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .reply(200, PUBLISHED_EXTENSIONS); - - const extensions = await extensionsApi.listExtensions(PUBLISHER_ID); - expect(extensions).to.deep.equal(PUBLISHED_EXTENSIONS.extensions); - expect(nock.isDone()).to.be.true; - }); - - it("should return a list of all extensions", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .reply(200, ALL_EXTENSIONS); - - const extensions = await extensionsApi.listExtensions(PUBLISHER_ID); - - expect(extensions).to.deep.equal(ALL_EXTENSIONS.extensions); - expect(nock.isDone()).to.be.true; - }); - - it("should query for more extensions if the response has a next_page_token", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .reply(200, PUBLISHED_WITH_TOKEN); - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - queryParams.pageToken === NEXT_PAGE_TOKEN; - return queryParams; - }) - .reply(200, NEXT_PAGE_EXTENSIONS); - - const extensions = await extensionsApi.listExtensions(PUBLISHER_ID); - - const expected = PUBLISHED_WITH_TOKEN.extensions.concat(NEXT_PAGE_EXTENSIONS.extensions); - expect(extensions).to.deep.equal(expected); - expect(nock.isDone()).to.be.true; - }); - - it("should throw FirebaseError if any call returns an error", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .reply(503, PUBLISHED_EXTENSIONS); - - await expect(extensionsApi.listExtensions(PUBLISHER_ID)).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); -}); - -describe("listExtensionVersions", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should return a list of published extension versions", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - return queryParams.pageSize === "100"; - }) - .reply(200, PUBLISHED_EXT_VERSIONS); - - const extensions = await extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); - expect(extensions).to.deep.equal(PUBLISHED_EXT_VERSIONS.extensionVersions); - expect(nock.isDone()).to.be.true; - }); - - it("should send filter query param", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - return queryParams.pageSize === "100" && queryParams.filter === "id<1.0.0"; - }) - .reply(200, PUBLISHED_EXT_VERSIONS); - - const extensions = await extensionsApi.listExtensionVersions( - `${PUBLISHER_ID}/${EXTENSION_ID}`, - "id<1.0.0" - ); - expect(extensions).to.deep.equal(PUBLISHED_EXT_VERSIONS.extensionVersions); - expect(nock.isDone()).to.be.true; - }); - - it("should return a list of all extension versions", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - return queryParams.pageSize === "100"; - }) - .reply(200, ALL_EXT_VERSIONS); - - const extensions = await extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); - - expect(extensions).to.deep.equal(ALL_EXT_VERSIONS.extensionVersions); - expect(nock.isDone()).to.be.true; - }); - - it("should query for more extension versions if the response has a next_page_token", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - return queryParams.pageSize === "100"; - }) - .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - return queryParams.pageSize === "100" && queryParams.pageToken === NEXT_PAGE_TOKEN; - }) - .reply(200, NEXT_PAGE_VERSIONS); - - const extensions = await extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`); - - const expected = PUBLISHED_VERSIONS_WITH_TOKEN.extensionVersions.concat( - NEXT_PAGE_VERSIONS.extensionVersions - ); - expect(extensions).to.deep.equal(expected); - expect(nock.isDone()).to.be.true; - }); - - it("should throw FirebaseError if any call returns an error", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - return queryParams.pageSize === "100"; - }) - .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - return queryParams.pageSize === "100" && queryParams.pageToken === NEXT_PAGE_TOKEN; - }) - .reply(500); - - await expect( - extensionsApi.listExtensionVersions(`${PUBLISHER_ID}/${EXTENSION_ID}`) - ).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error for an invalid ref", async () => { - await expect(extensionsApi.listExtensionVersions("")).to.be.rejectedWith( - FirebaseError, - "Unable to parse" - ); - }); -}); - -describe("registerPublisherProfile", () => { - afterEach(() => { - nock.cleanAll(); - }); - - const PUBLISHER_PROFILE = { - name: "projects/test-publisher/publisherProfile", - publisherId: "test-publisher", - registerTime: "2020-06-30T00:21:06.722782Z", - }; - it("should make a POST call to the correct endpoint", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/publisherProfile:register`) - .reply(200, PUBLISHER_PROFILE); - - const res = await extensionsApi.registerPublisherProfile(PROJECT_ID, PUBLISHER_ID); - expect(res).to.deep.equal(PUBLISHER_PROFILE); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - nock(api.extensionsOrigin) - .post(`/${VERSION}/projects/${PROJECT_ID}/publisherProfile:register`) - .reply(404); - await expect( - extensionsApi.registerPublisherProfile(PROJECT_ID, PUBLISHER_ID) - ).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); -}); - -describe("deprecateExtensionVersion", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a POST call to the correct endpoint", async () => { - const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_4.ref); - nock(api.extensionsOrigin) - .persist() - .post( - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:deprecate` - ) - .reply(200, TEST_EXT_VERSION_4); - - const res = await extensionsApi.deprecateExtensionVersion( - TEST_EXT_VERSION_4.ref, - "This version is deprecated." - ); - expect(res).to.deep.equal(TEST_EXT_VERSION_4); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_4.ref); - nock(api.extensionsOrigin) - .persist() - .post( - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:deprecate` - ) - .reply(404); - await expect( - extensionsApi.deprecateExtensionVersion(TEST_EXT_VERSION_4.ref, "This version is deprecated.") - ).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); -}); - -describe("undeprecateExtensionVersion", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should make a POST call to the correct endpoint", async () => { - const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_3.ref); - nock(api.extensionsOrigin) - .persist() - .post( - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:undeprecate` - ) - .reply(200, TEST_EXT_VERSION_3); - - const res = await extensionsApi.undeprecateExtensionVersion(TEST_EXT_VERSION_3.ref); - expect(res).to.deep.equal(TEST_EXT_VERSION_3); - expect(nock.isDone()).to.be.true; - }); - - it("should throw a FirebaseError if the endpoint returns an error response", async () => { - const { publisherId, extensionId, version } = refs.parse(TEST_EXT_VERSION_3.ref); - nock(api.extensionsOrigin) - .persist() - .post( - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}:undeprecate` - ) - .reply(404); - await expect( - extensionsApi.undeprecateExtensionVersion(TEST_EXT_VERSION_3.ref) - ).to.be.rejectedWith(FirebaseError); - expect(nock.isDone()).to.be.true; - }); -}); diff --git a/src/test/extensions/extensionsHelper.spec.ts b/src/test/extensions/extensionsHelper.spec.ts deleted file mode 100644 index 081bfb35802..00000000000 --- a/src/test/extensions/extensionsHelper.spec.ts +++ /dev/null @@ -1,887 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../error"; -import * as extensionsApi from "../../extensions/extensionsApi"; -import * as extensionsHelper from "../../extensions/extensionsHelper"; -import * as resolveSource from "../../extensions/resolveSource"; -import { storage } from "../../gcp"; -import * as archiveDirectory from "../../archiveDirectory"; -import * as prompt from "../../prompt"; -import { ExtensionSource } from "../../extensions/extensionsApi"; - -describe("extensionsHelper", () => { - describe("substituteParams", () => { - it("should substitute env variables", () => { - const testResources = [ - { - resourceOne: { - name: "${VAR_ONE}", - source: "path/${VAR_ONE}", - }, - }, - { - resourceTwo: { - property: "${VAR_TWO}", - another: "$NOT_ENV", - }, - }, - ]; - const testParam = { VAR_ONE: "foo", VAR_TWO: "bar", UNUSED: "faz" }; - expect(extensionsHelper.substituteParams(testResources, testParam)).to.deep.equal([ - { - resourceOne: { - name: "foo", - source: "path/foo", - }, - }, - { - resourceTwo: { - property: "bar", - another: "$NOT_ENV", - }, - }, - ]); - }); - }); - - it("should support both ${PARAM_NAME} AND ${param:PARAM_NAME} syntax", () => { - const testResources = [ - { - resourceOne: { - name: "${param:VAR_ONE}", - source: "path/${param:VAR_ONE}", - }, - }, - { - resourceTwo: { - property: "${param:VAR_TWO}", - another: "$NOT_ENV", - }, - }, - { - resourceThree: { - property: "${VAR_TWO}${VAR_TWO}${param:VAR_TWO}", - another: "${not:VAR_TWO}", - }, - }, - ]; - const testParam = { VAR_ONE: "foo", VAR_TWO: "bar", UNUSED: "faz" }; - expect(extensionsHelper.substituteParams(testResources, testParam)).to.deep.equal([ - { - resourceOne: { - name: "foo", - source: "path/foo", - }, - }, - { - resourceTwo: { - property: "bar", - another: "$NOT_ENV", - }, - }, - { - resourceThree: { - property: "barbarbar", - another: "${not:VAR_TWO}", - }, - }, - ]); - }); - - describe("getDBInstanceFromURL", () => { - it("returns the correct instance name", () => { - expect(extensionsHelper.getDBInstanceFromURL("https://my-db.firebaseio.com")).to.equal( - "my-db" - ); - }); - }); - - describe("populateDefaultParams", () => { - const expected = { - ENV_VAR_ONE: "12345", - ENV_VAR_TWO: "hello@example.com", - ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", - }; - - const exampleParamSpec: extensionsApi.Param[] = [ - { - param: "ENV_VAR_ONE", - label: "env1", - required: true, - }, - { - param: "ENV_VAR_TWO", - label: "env2", - required: true, - validationRegex: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", - validationErrorMessage: "You must provide a valid email address.\n", - }, - { - param: "ENV_VAR_THREE", - label: "env3", - default: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", - validationRegex: ".*\\{token\\}.*", - validationErrorMessage: - "Your URL must include {token} so that it can be replaced with an actual invitation token.\n", - }, - { - param: "ENV_VAR_FOUR", - label: "env4", - default: "users/{sender}.friends", - required: false, - validationRegex: ".+/.+\\..+", - validationErrorMessage: - "Values must be comma-separated document path + field, e.g. coll/doc.field,coll/doc.field\n", - }, - ]; - - it("should set default if default is available", () => { - const envFile = { - ENV_VAR_ONE: "12345", - ENV_VAR_TWO: "hello@example.com", - ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", - }; - - expect(extensionsHelper.populateDefaultParams(envFile, exampleParamSpec)).to.deep.equal( - expected - ); - }); - - it("should throw error if no default is available", () => { - const envFile = { - ENV_VAR_ONE: "12345", - ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", - ENV_VAR_FOUR: "users/{sender}.friends", - }; - - expect(() => { - extensionsHelper.populateDefaultParams(envFile, exampleParamSpec); - }).to.throw(FirebaseError, /no default available/); - }); - }); - - describe("validateCommandLineParams", () => { - const exampleParamSpec: extensionsApi.Param[] = [ - { - param: "ENV_VAR_ONE", - label: "env1", - required: true, - }, - { - param: "ENV_VAR_TWO", - label: "env2", - required: true, - validationRegex: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", - validationErrorMessage: "You must provide a valid email address.\n", - }, - { - param: "ENV_VAR_THREE", - label: "env3", - default: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", - validationRegex: ".*\\{token\\}.*", - validationErrorMessage: - "Your URL must include {token} so that it can be replaced with an actual invitation token.\n", - }, - { - param: "ENV_VAR_FOUR", - label: "env3", - default: "users/{sender}.friends", - required: false, - validationRegex: ".+/.+\\..+", - validationErrorMessage: - "Values must be comma-separated document path + field, e.g. coll/doc.field,coll/doc.field\n", - }, - ]; - - it("should throw error if param variable value is invalid", () => { - const envFile = { - ENV_VAR_ONE: "12345", - ENV_VAR_TWO: "invalid", - ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", - ENV_VAR_FOUR: "users/{sender}.friends", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(envFile, exampleParamSpec); - }).to.throw(FirebaseError, /not valid/); - }); - - it("should throw error if # commandLineParams does not match # env vars from extension.yaml", () => { - const envFile = { - ENV_VAR_ONE: "12345", - ENV_VAR_TWO: "invalid", - ENV_VAR_THREE: "https://${PROJECT_ID}.web.app/?acceptInvitation={token}", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(envFile, exampleParamSpec); - }).to.throw(FirebaseError); - }); - - it("should throw an error if a required param is missing", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - required: true, - }, - { - param: "BYE", - label: "goodbye", - required: false, - }, - ]; - const testParams = { - BYE: "val", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).to.throw(FirebaseError); - }); - - it("should not throw a error if a non-required param is missing", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - required: true, - }, - { - param: "BYE", - label: "goodbye", - required: false, - }, - ]; - const testParams = { - HI: "val", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).not.to.throw(); - }); - - it("should not throw a regex error if a non-required param is missing", () => { - const testParamSpec = [ - { - param: "BYE", - label: "goodbye", - required: false, - validationRegex: "FAIL", - }, - ]; - const testParams = {}; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).not.to.throw(); - }); - - it("should throw a error if a param value doesn't pass the validation regex", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - validationRegex: "FAIL", - required: true, - }, - ]; - const testParams = { - HI: "val", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).to.throw(FirebaseError); - }); - - it("should throw a error if a multiselect value isn't an option", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - type: extensionsApi.ParamType.MULTISELECT, - options: [ - { - value: "val", - }, - ], - required: true, - }, - ]; - const testParams = { - HI: "val,FAIL", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).to.throw(FirebaseError); - }); - - it("should throw a error if a multiselect param is missing options", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - type: extensionsApi.ParamType.MULTISELECT, - options: [], - validationRegex: "FAIL", - required: true, - }, - ]; - const testParams = { - HI: "FAIL,val", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).to.throw(FirebaseError); - }); - - it("should throw a error if a select param is missing options", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - type: extensionsApi.ParamType.SELECT, - validationRegex: "FAIL", - options: [], - required: true, - }, - ]; - const testParams = { - HI: "FAIL,val", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).to.throw(FirebaseError); - }); - - it("should not throw if a select value is an option", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - type: extensionsApi.ParamType.SELECT, - options: [ - { - value: "val", - }, - ], - required: true, - }, - ]; - const testParams = { - HI: "val", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).not.to.throw(); - }); - - it("should not throw if all multiselect values are options", () => { - const testParamSpec = [ - { - param: "HI", - label: "hello", - type: extensionsApi.ParamType.MULTISELECT, - options: [ - { - value: "val", - }, - { - value: "val2", - }, - ], - required: true, - }, - ]; - const testParams = { - HI: "val,val2", - }; - - expect(() => { - extensionsHelper.validateCommandLineParams(testParams, testParamSpec); - }).not.to.throw(); - }); - }); - - describe("validateSpec", () => { - it("should not error on a valid spec", () => { - const testSpec: extensionsApi.ExtensionSpec = { - name: "test", - version: "0.1.0", - specVersion: "v1beta", - resources: [], - params: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).not.to.throw(); - }); - it("should error if license is missing", () => { - const testSpec: extensionsApi.ExtensionSpec = { - name: "test", - version: "0.1.0", - specVersion: "v1beta", - resources: [], - params: [], - sourceUrl: "https://test-source.fake", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /license/); - }); - it("should error if license is invalid", () => { - const testSpec: extensionsApi.ExtensionSpec = { - name: "test", - version: "0.1.0", - specVersion: "v1beta", - resources: [], - params: [], - sourceUrl: "https://test-source.fake", - license: "invalid-license", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /license/); - }); - it("should error if name is missing", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /name/); - }); - - it("should error if specVersion is missing", () => { - const testSpec = { - name: "test", - version: "0.1.0", - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /specVersion/); - }); - - it("should error if version is missing", () => { - const testSpec = { - name: "test", - specVersion: "v1beta", - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /version/); - }); - - it("should error if a resource is malformed", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - resources: [{}], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /name/); - }); - - it("should error if an api is malformed", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - apis: [{}], - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /apiName/); - }); - - it("should error if a param is malformed", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - params: [{}], - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /param/); - }); - - it("should error if a STRING param has options.", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - params: [{ options: [] }], - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /options/); - }); - - it("should error if a select param has validationRegex.", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - params: [{ type: extensionsHelper.SpecParamType.SELECT, validationRegex: "test" }], - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /validationRegex/); - }); - it("should error if a param has an invalid type.", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - params: [{ type: "test-type", validationRegex: "test" }], - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /Invalid type/); - }); - it("should error if a param selectResource missing resourceType.", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - params: [ - { - type: extensionsHelper.SpecParamType.SELECTRESOURCE, - validationRegex: "test", - default: "fail", - }, - ], - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /must have resourceType/); - }); - }); - - describe("promptForValidInstanceId", () => { - let promptStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should prompt the user and return if the user provides a valid id", async () => { - const extensionName = "extension-name"; - const userInput = "a-valid-name"; - promptStub.returns(userInput); - - const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); - - expect(instanceId).to.equal(userInput); - expect(promptStub).to.have.been.calledOnce; - }); - - it("should prompt the user again if the provided id is shorter than 6 characters", async () => { - const extensionName = "extension-name"; - const userInput1 = "short"; - const userInput2 = "a-valid-name"; - promptStub.onCall(0).returns(userInput1); - promptStub.onCall(1).returns(userInput2); - - const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); - - expect(instanceId).to.equal(userInput2); - expect(promptStub).to.have.been.calledTwice; - }); - - it("should prompt the user again if the provided id is longer than 45 characters", async () => { - const extensionName = "extension-name"; - const userInput1 = "a-really-long-name-that-is-really-longer-than-were-ok-with"; - const userInput2 = "a-valid-name"; - promptStub.onCall(0).returns(userInput1); - promptStub.onCall(1).returns(userInput2); - - const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); - - expect(instanceId).to.equal(userInput2); - expect(promptStub).to.have.been.calledTwice; - }); - - it("should prompt the user again if the provided id ends in a -", async () => { - const extensionName = "extension-name"; - const userInput1 = "invalid-"; - const userInput2 = "-invalid"; - const userInput3 = "a-valid-name"; - promptStub.onCall(0).returns(userInput1); - promptStub.onCall(1).returns(userInput2); - promptStub.onCall(2).returns(userInput3); - - const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); - - expect(instanceId).to.equal(userInput3); - expect(promptStub).to.have.been.calledThrice; - }); - - it("should prompt the user again if the provided id starts with a number", async () => { - const extensionName = "extension-name"; - const userInput1 = "1invalid"; - const userInput2 = "a-valid-name"; - promptStub.onCall(0).returns(userInput1); - promptStub.onCall(1).returns(userInput2); - - const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); - - expect(instanceId).to.equal(userInput2); - expect(promptStub).to.have.been.calledTwice; - }); - - it("should prompt the user again if the provided id contains illegal characters", async () => { - const extensionName = "extension-name"; - const userInput1 = "na.name@name"; - const userInput2 = "a-valid-name"; - promptStub.onCall(0).returns(userInput1); - promptStub.onCall(1).returns(userInput2); - - const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); - - expect(instanceId).to.equal(userInput2); - expect(promptStub).to.have.been.calledTwice; - }); - }); - - describe("createSourceFromLocation", () => { - let archiveStub: sinon.SinonStub; - let uploadStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let deleteStub: sinon.SinonStub; - const testUrl = "https://storage.googleapis.com/firebase-ext-eap-uploads/object.zip"; - const testSource: ExtensionSource = { - name: "test", - packageUri: testUrl, - hash: "abc123", - state: "ACTIVE", - spec: { - name: "projects/test-proj/sources/abc123", - version: "0.0.0", - sourceUrl: testUrl, - resources: [], - params: [], - }, - }; - - beforeEach(() => { - archiveStub = sinon.stub(archiveDirectory, "archiveDirectory").resolves({}); - uploadStub = sinon.stub(storage, "uploadObject").resolves({ - bucket: "firebase-ext-eap-uploads", - object: "object.zip", - generation: 42, - }); - createSourceStub = sinon.stub(extensionsApi, "createSource").resolves(testSource); - deleteStub = sinon.stub(storage, "deleteObject").resolves(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should upload local sources to Firebase Storage then create an ExtensionSource", async () => { - const result = await extensionsHelper.createSourceFromLocation("test-proj", "."); - - expect(result).to.equal(testSource); - expect(archiveStub).to.have.been.calledWith("."); - expect(uploadStub).to.have.been.calledWith({}, extensionsHelper.EXTENSIONS_BUCKET_NAME); - expect(createSourceStub).to.have.been.calledWith("test-proj", testUrl + "?alt=media", "/"); - expect(deleteStub).to.have.been.calledWith( - `/${extensionsHelper.EXTENSIONS_BUCKET_NAME}/object.zip` - ); - }); - - it("should succeed even when it fails to delete the uploaded archive", async () => { - deleteStub.throws(); - - const result = await extensionsHelper.createSourceFromLocation("test-proj", "."); - - expect(result).to.equal(testSource); - expect(archiveStub).to.have.been.calledWith("."); - expect(uploadStub).to.have.been.calledWith({}, extensionsHelper.EXTENSIONS_BUCKET_NAME); - expect(createSourceStub).to.have.been.calledWith("test-proj", testUrl + "?alt=media", "/"); - expect(deleteStub).to.have.been.calledWith( - `/${extensionsHelper.EXTENSIONS_BUCKET_NAME}/object.zip` - ); - }); - - it("should create an ExtensionSource with url sources", async () => { - const url = "https://storage.com/my.zip"; - - const result = await extensionsHelper.createSourceFromLocation("test-proj", url); - - expect(result).to.equal(testSource); - expect(createSourceStub).to.have.been.calledWith("test-proj", url); - expect(archiveStub).not.to.have.been.called; - expect(uploadStub).not.to.have.been.called; - expect(deleteStub).not.to.have.been.called; - }); - - it("should throw an error if one is thrown while uploading a local source ", async () => { - uploadStub.throws(new FirebaseError("something bad happened")); - - await expect(extensionsHelper.createSourceFromLocation("test-proj", ".")).to.be.rejectedWith( - FirebaseError - ); - - expect(archiveStub).to.have.been.calledWith("."); - expect(uploadStub).to.have.been.calledWith({}, extensionsHelper.EXTENSIONS_BUCKET_NAME); - expect(createSourceStub).not.to.have.been.called; - expect(deleteStub).not.to.have.been.called; - }); - }); - - describe("getExtensionSourceFromName", () => { - let resolveRegistryEntryStub: sinon.SinonStub; - let getSourceStub: sinon.SinonStub; - - const testOnePlatformSourceName = "projects/test-proj/sources/abc123"; - const testRegistyEntry = { - labels: { latest: "0.1.1" }, - versions: { - "0.1.0": "projects/test-proj/sources/def456", - "0.1.1": testOnePlatformSourceName, - }, - publisher: "firebase", - }; - const testSource: ExtensionSource = { - name: "test", - packageUri: "", - hash: "abc123", - state: "ACTIVE", - spec: { - name: "", - version: "0.0.0", - sourceUrl: "", - resources: [], - params: [], - }, - }; - - beforeEach(() => { - resolveRegistryEntryStub = sinon - .stub(resolveSource, "resolveRegistryEntry") - .resolves(testRegistyEntry); - getSourceStub = sinon.stub(extensionsApi, "getSource").resolves(testSource); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should look up official source names in the registry and fetch the ExtensionSource found there", async () => { - const testOfficialName = "storage-resize-images"; - - const result = await extensionsHelper.getExtensionSourceFromName(testOfficialName); - - expect(resolveRegistryEntryStub).to.have.been.calledWith(testOfficialName); - expect(getSourceStub).to.have.been.calledWith(testOnePlatformSourceName); - expect(result).to.equal(testSource); - }); - - it("should fetch ExtensionSources when given a one platform name", async () => { - const result = await extensionsHelper.getExtensionSourceFromName(testOnePlatformSourceName); - - expect(resolveRegistryEntryStub).not.to.have.been.called; - expect(getSourceStub).to.have.been.calledWith(testOnePlatformSourceName); - expect(result).to.equal(testSource); - }); - - it("should throw an error if given a invalid namae", async () => { - await expect(extensionsHelper.getExtensionSourceFromName(".")).to.be.rejectedWith( - FirebaseError - ); - - expect(resolveRegistryEntryStub).not.to.have.been.called; - expect(getSourceStub).not.to.have.been.called; - }); - }); - - describe("checkIfInstanceIdAlreadyExists", () => { - const TEST_NAME = "image-resizer"; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - getInstanceStub = sinon.stub(extensionsApi, "getInstance"); - }); - - afterEach(() => { - getInstanceStub.restore(); - }); - - it("should return false if no instance with that name exists", async () => { - getInstanceStub.resolves({ error: { code: 404 } }); - - const exists = await extensionsHelper.instanceIdExists("proj", TEST_NAME); - expect(exists).to.be.false; - }); - - it("should return true if an instance with that name exists", async () => { - getInstanceStub.resolves({ name: TEST_NAME }); - - const exists = await extensionsHelper.instanceIdExists("proj", TEST_NAME); - expect(exists).to.be.true; - }); - - it("should throw if it gets an unexpected error response from getInstance", async () => { - getInstanceStub.resolves({ error: { code: 500, message: "a message" } }); - - await expect(extensionsHelper.instanceIdExists("proj", TEST_NAME)).to.be.rejectedWith( - FirebaseError, - "Unexpected error when checking if instance ID exists: a message" - ); - }); - }); -}); diff --git a/src/test/extensions/localHelper.spec.ts b/src/test/extensions/localHelper.spec.ts deleted file mode 100644 index 396fa269b1d..00000000000 --- a/src/test/extensions/localHelper.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { expect } from "chai"; -import * as fs from "fs-extra"; -import * as yaml from "js-yaml"; -import { resolve } from "path"; -import * as sinon from "sinon"; - -import * as localHelper from "../../extensions/localHelper"; -import { FirebaseError } from "../../error"; - -const EXT_FIXTURE_DIRECTORY = resolve(__dirname, "../fixtures/sample-ext"); -const EXT_PREINSTALL_FIXTURE_DIRECTORY = resolve(__dirname, "../fixtures/sample-ext-preinstall"); - -describe("localHelper", () => { - const sandbox = sinon.createSandbox(); - - describe("getLocalExtensionSpec", () => { - it("should return a spec when extension.yaml is present", async () => { - const result = await localHelper.getLocalExtensionSpec(EXT_FIXTURE_DIRECTORY); - expect(result.name).to.equal("fixture-ext"); - expect(result.version).to.equal("1.0.0"); - expect(result.preinstallContent).to.be.undefined; - }); - - it("should populate preinstallContent when PREINSTALL.md is present", async () => { - const result = await localHelper.getLocalExtensionSpec(EXT_PREINSTALL_FIXTURE_DIRECTORY); - expect(result.name).to.equal("fixture-ext-with-preinstall"); - expect(result.version).to.equal("1.0.0"); - expect(result.preinstallContent).to.equal("This is a PREINSTALL file for testing with.\n"); - }); - - it("should return a nice error if there is no extension.yaml", async () => { - await expect(localHelper.getLocalExtensionSpec(__dirname)).to.be.rejectedWith(FirebaseError); - }); - - describe("with an invalid YAML file", () => { - beforeEach(() => { - sandbox.stub(fs, "readFileSync").returns(`name: foo\nunknownkey\nother: value`); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should return a rejected promise with a useful error if extension.yaml is invalid", async () => { - await expect(localHelper.getLocalExtensionSpec(EXT_FIXTURE_DIRECTORY)).to.be.rejectedWith( - FirebaseError, - /YAML Error.+multiline key.+line.+/ - ); - }); - }); - - describe("other YAML errors", () => { - beforeEach(() => { - sandbox.stub(yaml, "safeLoad").throws(new Error("not the files you are looking for")); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should rethrow normal errors", async () => { - await expect(localHelper.getLocalExtensionSpec(EXT_FIXTURE_DIRECTORY)).to.be.rejectedWith( - FirebaseError, - "not the files you are looking for" - ); - }); - }); - }); - - describe("isLocalExtension", () => { - let fsStub: sinon.SinonStub; - beforeEach(() => { - fsStub = sandbox.stub(fs, "readdirSync"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should return true if a file exists there", () => { - fsStub.returns(""); - - const result = localHelper.isLocalExtension("some/local/path"); - - expect(result).to.be.true; - }); - - it("should return false if a file doesn't exist there", () => { - fsStub.throws(new Error("directory not found")); - - const result = localHelper.isLocalExtension("some/local/path"); - - expect(result).to.be.false; - }); - }); -}); diff --git a/src/test/extensions/paramHelper.spec.ts b/src/test/extensions/paramHelper.spec.ts deleted file mode 100644 index b6ecb183b84..00000000000 --- a/src/test/extensions/paramHelper.spec.ts +++ /dev/null @@ -1,504 +0,0 @@ -import * as _ from "lodash"; -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as fs from "fs-extra"; - -import { FirebaseError } from "../../error"; -import { logger } from "../../logger"; -import { ExtensionInstance, Param, ParamType } from "../../extensions/extensionsApi"; -import * as extensionsHelper from "../../extensions/extensionsHelper"; -import * as paramHelper from "../../extensions/paramHelper"; -import * as env from "../../functions/env"; -import * as prompt from "../../prompt"; - -const PROJECT_ID = "test-proj"; -const INSTANCE_ID = "ext-instance"; -const TEST_PARAMS: Param[] = [ - { - param: "A_PARAMETER", - label: "Param", - type: ParamType.STRING, - required: true, - }, - { - param: "ANOTHER_PARAMETER", - label: "Another Param", - default: "default", - type: ParamType.STRING, - required: true, - }, -]; - -const TEST_PARAMS_2: Param[] = [ - { - param: "ANOTHER_PARAMETER", - label: "Another Param", - type: ParamType.STRING, - default: "default", - }, - { - param: "NEW_PARAMETER", - label: "New Param", - type: ParamType.STRING, - default: "${PROJECT_ID}", - }, - { - param: "THIRD_PARAMETER", - label: "3", - type: ParamType.STRING, - default: "default", - }, -]; -const TEST_PARAMS_3: Param[] = [ - { - param: "A_PARAMETER", - label: "Param", - type: ParamType.STRING, - }, - { - param: "ANOTHER_PARAMETER", - label: "Another Param", - default: "default", - type: ParamType.STRING, - description: "Something new", - required: false, - }, -]; - -const SPEC = { - name: "test", - version: "0.1.0", - roles: [], - resources: [], - sourceUrl: "test.com", - params: TEST_PARAMS, -}; - -describe("paramHelper", () => { - describe("getParams", () => { - let envStub: sinon.SinonStub; - let promptStub: sinon.SinonStub; - let loggerSpy: sinon.SinonSpy; - - beforeEach(() => { - sinon.stub(fs, "readFileSync").returns(""); - envStub = sinon.stub(env, "parse"); - sinon.stub(extensionsHelper, "getFirebaseProjectParams").resolves({ PROJECT_ID }); - promptStub = sinon.stub(prompt, "promptOnce").resolves("user input"); - loggerSpy = sinon.spy(logger, "warn"); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should read params from envFilePath if it is provided and is valid", async () => { - envStub.returns({ - envs: { - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "value", - }, - errors: [], - }); - - const params = await paramHelper.getParams({ - projectId: PROJECT_ID, - paramSpecs: TEST_PARAMS, - nonInteractive: false, - paramsEnvPath: "./a/path/to/a/file.env", - instanceId: INSTANCE_ID, - }); - - expect(params).to.eql({ - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "value", - }); - }); - - it("should return the defaults for params that are not in envFilePath", async () => { - envStub.returns({ - envs: { - A_PARAMETER: "aValue", - }, - errors: [], - }); - - const params = await paramHelper.getParams({ - projectId: PROJECT_ID, - paramSpecs: TEST_PARAMS, - nonInteractive: false, - paramsEnvPath: "./a/path/to/a/file.env", - instanceId: INSTANCE_ID, - }); - - expect(params).to.eql({ - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "default", - }); - }); - - it("should omit optional params that are not in envFilePath", async () => { - envStub.returns({ - envs: { - A_PARAMETER: "aValue", - }, - errors: [], - }); - - const params = await paramHelper.getParams({ - projectId: PROJECT_ID, - paramSpecs: TEST_PARAMS_3, - nonInteractive: false, - paramsEnvPath: "./a/path/to/a/file.env", - instanceId: INSTANCE_ID, - }); - - expect(params).to.eql({ - A_PARAMETER: "aValue", - }); - }); - - it("should throw if a required param without a default is not in envFilePath", async () => { - envStub.returns({ - envs: { - ANOTHER_PARAMETER: "aValue", - }, - errors: [], - }); - - await expect( - paramHelper.getParams({ - projectId: PROJECT_ID, - paramSpecs: TEST_PARAMS, - nonInteractive: false, - paramsEnvPath: "./a/path/to/a/file.env", - instanceId: INSTANCE_ID, - }) - ).to.be.rejectedWith( - FirebaseError, - "A_PARAMETER has not been set in the given params file and there is no default available. " + - "Please set this variable before installing again." - ); - }); - - it("should warn about extra params provided in the env file", async () => { - envStub.returns({ - envs: { - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "default", - A_THIRD_PARAMETER: "aValue", - A_FOURTH_PARAMETER: "default", - }, - errors: [], - }); - await paramHelper.getParams({ - projectId: PROJECT_ID, - paramSpecs: TEST_PARAMS, - nonInteractive: false, - paramsEnvPath: "./a/path/to/a/file.env", - instanceId: INSTANCE_ID, - }); - - expect(loggerSpy).to.have.been.calledWith( - "Warning: The following params were specified in your env file but" + - " do not exist in the extension spec: A_THIRD_PARAMETER, A_FOURTH_PARAMETER." - ); - }); - - it("should throw FirebaseError if an invalid envFilePath is provided", async () => { - envStub.returns({ - envs: {}, - errors: ["An error"], - }); - - await expect( - paramHelper.getParams({ - projectId: PROJECT_ID, - paramSpecs: TEST_PARAMS, - nonInteractive: false, - paramsEnvPath: "./a/path/to/a/file.env", - instanceId: INSTANCE_ID, - }) - ).to.be.rejectedWith(FirebaseError, "Error reading env file"); - }); - - it("should prompt the user for params if no env file is provided", async () => { - const params = await paramHelper.getParams({ - projectId: PROJECT_ID, - paramSpecs: TEST_PARAMS, - instanceId: INSTANCE_ID, - }); - - expect(params).to.eql({ - A_PARAMETER: "user input", - ANOTHER_PARAMETER: "user input", - }); - - expect(promptStub).to.have.been.calledTwice; - expect(promptStub.firstCall.args[0]).to.eql({ - default: undefined, - message: "Enter a value for Param:", - name: "A_PARAMETER", - type: "input", - }); - expect(promptStub.secondCall.args[0]).to.eql({ - default: "default", - message: "Enter a value for Another Param:", - name: "ANOTHER_PARAMETER", - type: "input", - }); - }); - }); - - describe("getParamsWithCurrentValuesAsDefaults", () => { - let params: { [key: string]: string }; - let testInstance: ExtensionInstance; - beforeEach(() => { - params = { A_PARAMETER: "new default" }; - testInstance = { - config: { - source: { - state: "ACTIVE", - name: "", - packageUri: "", - hash: "", - spec: { - name: "", - version: "0.1.0", - roles: [], - resources: [], - params: TEST_PARAMS, - sourceUrl: "", - }, - }, - name: "test", - createTime: "now", - params, - }, - name: "test", - createTime: "now", - updateTime: "now", - state: "ACTIVE", - serviceAccountEmail: "test@test.com", - }; - - it("should add defaults to params without them using the current state and leave other values unchanged", () => { - const newParams = paramHelper.getParamsWithCurrentValuesAsDefaults(testInstance); - - expect(newParams).to.eql([ - { - param: "A_PARAMETER", - label: "Param", - default: "new default", - type: ParamType.STRING, - required: true, - }, - { - param: "ANOTHER_PARAMETER", - label: "Another", - default: "default", - type: ParamType.STRING, - }, - ]); - }); - }); - - it("should change existing defaults to the current state and leave other values unchanged", () => { - _.get(testInstance, "config.source.spec.params", []).push({ - param: "THIRD", - label: "3rd", - default: "default", - type: ParamType.STRING, - }); - testInstance.config.params.THIRD = "New Default"; - const newParams = paramHelper.getParamsWithCurrentValuesAsDefaults(testInstance); - - expect(newParams).to.eql([ - { - param: "A_PARAMETER", - label: "Param", - default: "new default", - type: ParamType.STRING, - required: true, - }, - { - param: "ANOTHER_PARAMETER", - label: "Another Param", - default: "default", - required: true, - type: ParamType.STRING, - }, - { - param: "THIRD", - label: "3rd", - default: "New Default", - type: ParamType.STRING, - }, - ]); - }); - }); - - describe("promptForNewParams", () => { - let promptStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - sinon.stub(extensionsHelper, "getFirebaseProjectParams").resolves({ PROJECT_ID }); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should prompt the user for any params in the new spec that are not in the current one", async () => { - promptStub.resolves("user input"); - const newSpec = _.cloneDeep(SPEC); - newSpec.params = TEST_PARAMS_2; - - const newParams = await paramHelper.promptForNewParams({ - spec: SPEC, - newSpec, - currentParams: { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - }); - - const expected = { - ANOTHER_PARAMETER: "value", - NEW_PARAMETER: "user input", - THIRD_PARAMETER: "user input", - }; - expect(newParams).to.eql(expected); - expect(promptStub.callCount).to.equal(2); - expect(promptStub.firstCall.args).to.eql([ - { - default: "test-proj", - message: "Enter a value for New Param:", - name: "NEW_PARAMETER", - type: "input", - }, - ]); - expect(promptStub.secondCall.args).to.eql([ - { - default: "default", - message: "Enter a value for 3:", - name: "THIRD_PARAMETER", - type: "input", - }, - ]); - }); - - it("should not prompt the user for params that did not change type or param", async () => { - promptStub.resolves("Fail"); - const newSpec = _.cloneDeep(SPEC); - newSpec.params = TEST_PARAMS_3; - - const newParams = await paramHelper.promptForNewParams({ - spec: SPEC, - newSpec, - currentParams: { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - }); - - const expected = { - ANOTHER_PARAMETER: "value", - A_PARAMETER: "value", - }; - expect(newParams).to.eql(expected); - expect(promptStub).not.to.have.been.called; - }); - - it("should populate the spec with the default value if it is returned by prompt", async () => { - promptStub.onFirstCall().resolves("test-proj"); - promptStub.onSecondCall().resolves("user input"); - const newSpec = _.cloneDeep(SPEC); - newSpec.params = TEST_PARAMS_2; - - const newParams = await paramHelper.promptForNewParams({ - spec: SPEC, - newSpec, - currentParams: { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - }); - - const expected = { - ANOTHER_PARAMETER: "value", - NEW_PARAMETER: "test-proj", - THIRD_PARAMETER: "user input", - }; - expect(newParams).to.eql(expected); - expect(promptStub.callCount).to.equal(2); - expect(promptStub.firstCall.args).to.eql([ - { - default: "test-proj", - message: "Enter a value for New Param:", - name: "NEW_PARAMETER", - type: "input", - }, - ]); - expect(promptStub.secondCall.args).to.eql([ - { - default: "default", - message: "Enter a value for 3:", - name: "THIRD_PARAMETER", - type: "input", - }, - ]); - }); - - it("shouldn't prompt if there are no new params", async () => { - promptStub.resolves("Fail"); - const newSpec = _.cloneDeep(SPEC); - - const newParams = await paramHelper.promptForNewParams({ - spec: SPEC, - newSpec, - currentParams: { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - }); - - const expected = { - ANOTHER_PARAMETER: "value", - A_PARAMETER: "value", - }; - expect(newParams).to.eql(expected); - expect(promptStub).not.to.have.been.called; - }); - - it("should exit if a prompt fails", async () => { - promptStub.rejects(new FirebaseError("this is an error")); - const newSpec = _.cloneDeep(SPEC); - newSpec.params = TEST_PARAMS_2; - - await expect( - paramHelper.promptForNewParams({ - spec: SPEC, - newSpec, - currentParams: { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - projectId: PROJECT_ID, - instanceId: INSTANCE_ID, - }) - ).to.be.rejectedWith(FirebaseError, "this is an error"); - // Ensure that we don't continue prompting if one fails - expect(promptStub).to.have.been.calledOnce; - }); - }); -}); diff --git a/src/test/extensions/resolveSource.spec.ts b/src/test/extensions/resolveSource.spec.ts deleted file mode 100644 index eba11b31639..00000000000 --- a/src/test/extensions/resolveSource.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as resolveSource from "../../extensions/resolveSource"; - -const testRegistryEntry = { - name: "test-stuff", - labels: { - latest: "0.2.0", - }, - publisher: "firebase", - versions: { - "0.1.0": "projects/firebasemods/sources/2kd", - "0.1.1": "projects/firebasemods/sources/xyz", - "0.1.2": "projects/firebasemods/sources/123", - "0.2.0": "projects/firebasemods/sources/abc", - }, - updateWarnings: { - ">0.1.0 <0.2.0": [ - { - from: "0.1.0", - description: - "Starting Jan 15, HTTP functions will be private by default. [Learn more](https://someurl.com)", - action: - "After updating, it is highly recommended that you switch your Cloud Scheduler jobs to PubSub", - }, - ], - ">=0.2.0": [ - { - from: "0.1.0", - description: - "Starting Jan 15, HTTP functions will be private by default. [Learn more](https://someurl.com)", - action: - "After updating, you must switch your Cloud Scheduler jobs to PubSub, otherwise your extension will stop running.", - }, - { - from: ">0.1.0", - description: - "Starting Jan 15, HTTP functions will be private by default. [Learn more](https://someurl.com)", - action: - "If you have not already done so during a previous update, after updating, you must switch your Cloud Scheduler jobs to PubSub, otherwise your extension will stop running.", - }, - ], - }, -}; - -describe("isPublishedSource", () => { - it("should return true for an published source", () => { - const result = resolveSource.isOfficialSource( - testRegistryEntry, - "projects/firebasemods/sources/2kd" - ); - expect(result).to.be.true; - }); - - it("should return false for an unpublished source", () => { - const result = resolveSource.isOfficialSource( - testRegistryEntry, - "projects/firebasemods/sources/invalid" - ); - expect(result).to.be.false; - }); -}); diff --git a/src/test/extensions/secretUtils.spec.ts b/src/test/extensions/secretUtils.spec.ts deleted file mode 100644 index b5907735708..00000000000 --- a/src/test/extensions/secretUtils.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as nock from "nock"; -import { expect } from "chai"; - -import * as api from "../../api"; -import * as extensionsApi from "../../extensions/extensionsApi"; -import * as secretsUtils from "../../extensions/secretsUtils"; - -const PROJECT_ID = "test-project"; -const TEST_INSTANCE: extensionsApi.ExtensionInstance = { - name: "projects/invader-zim/instances/image-resizer", - createTime: "2019-05-19T00:20:10.416947Z", - updateTime: "2019-05-19T00:20:10.416947Z", - state: "ACTIVE", - serviceAccountEmail: "service@account.com", - config: { - name: - "projects/invader-zim/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", - createTime: "2019-05-19T00:20:10.416947Z", - source: { - name: "", - state: "ACTIVE", - packageUri: "url", - hash: "hash", - spec: { - name: "test", - displayName: "Old", - description: "descriptive", - version: "1.0.0", - license: "MIT", - resources: [], - author: { authorName: "Tester" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [ - { - param: "SECRET1", - label: "secret 1", - type: extensionsApi.ParamType.SECRET, - }, - { - param: "SECRET2", - label: "secret 2", - type: extensionsApi.ParamType.SECRET, - }, - ], - }, - }, - params: { - SECRET1: "projects/test-project/secrets/secret1/versions/1", - SECRET2: "projects/test-project/secrets/secret2/versions/1", - }, - }, -}; - -describe("secretsUtils", () => { - afterEach(() => { - nock.cleanAll(); - }); - - describe("getManagedSecrets", () => { - it("only returns secrets that have labels set", async () => { - nock(api.secretManagerOrigin) - .get(`/v1beta1/projects/${PROJECT_ID}/secrets/secret1`) - .reply(200, { - name: `projects/${PROJECT_ID}/secrets/secret1`, - labels: { "firebase-extensions-managed": "true" }, - }); - nock(api.secretManagerOrigin) - .get(`/v1beta1/projects/${PROJECT_ID}/secrets/secret2`) - .reply(200, { - name: `projects/${PROJECT_ID}/secrets/secret2`, - }); // no labels - - expect(await secretsUtils.getManagedSecrets(TEST_INSTANCE)).to.deep.equal([ - "projects/test-project/secrets/secret1/versions/1", - ]); - - expect(nock.isDone()).to.be.true; - }); - }); -}); diff --git a/src/test/extensions/updateHelper.spec.ts b/src/test/extensions/updateHelper.spec.ts deleted file mode 100644 index 2c110b7389f..00000000000 --- a/src/test/extensions/updateHelper.spec.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { expect } from "chai"; -import * as nock from "nock"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../error"; -import { firebaseExtensionsRegistryOrigin } from "../../api"; -import * as extensionsApi from "../../extensions/extensionsApi"; -import * as extensionsHelper from "../../extensions/extensionsHelper"; -import * as prompt from "../../prompt"; -import * as resolveSource from "../../extensions/resolveSource"; -import * as updateHelper from "../../extensions/updateHelper"; - -const SPEC = { - name: "test", - displayName: "Old", - description: "descriptive", - version: "0.2.0", - license: "MIT", - apis: [ - { apiName: "api1", reason: "" }, - { apiName: "api2", reason: "" }, - ], - roles: [ - { role: "role1", reason: "" }, - { role: "role2", reason: "" }, - ], - resources: [ - { name: "resource1", type: "firebaseextensions.v1beta.function", description: "desc" }, - { name: "resource2", type: "other", description: "" }, - ], - author: { authorName: "Tester" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [], -}; - -const OLD_SPEC = Object.assign({}, SPEC, { version: "0.1.0" }); - -const SOURCE = { - name: "projects/firebasemods/sources/new-test-source", - packageUri: "https://firebase-fake-bucket.com", - hash: "1234567", - spec: SPEC, -}; - -const EXTENSION_VERSION = { - name: "publishers/test-publisher/extensions/test/versions/0.2.0", - ref: "test-publisher/test@0.2.0", - spec: SPEC, - state: "PUBLISHED", - hash: "abcdefg", - createTime: "2020-06-30T00:21:06.722782Z", -}; - -const EXTENSION = { - name: "publishers/test-publisher/extensions/test", - ref: "test-publisher/test", - state: "PUBLISHED", - createTime: "2020-06-30T00:21:06.722782Z", - latestVersion: "0.2.0", -}; - -const REGISTRY_ENTRY = { - name: "test", - labels: { - latest: "0.2.0", - minRequired: "0.1.1", - }, - versions: { - "0.1.0": "projects/firebasemods/sources/2kd", - "0.1.1": "projects/firebasemods/sources/xyz", - "0.1.2": "projects/firebasemods/sources/123", - "0.2.0": "projects/firebasemods/sources/abc", - }, - updateWarnings: { - ">0.1.0 <0.2.0": [ - { - from: "0.1.0", - description: - "Starting Jan 15, HTTP functions will be private by default. [Learn more](https://someurl.com)", - action: - "After updating, it is highly recommended that you switch your Cloud Scheduler jobs to PubSub", - }, - ], - ">=0.2.0": [ - { - from: "0.1.0", - description: - "Starting Jan 15, HTTP functions will be private by default. [Learn more](https://someurl.com)", - action: - "After updating, you must switch your Cloud Scheduler jobs to PubSub, otherwise your extension will stop running.", - }, - { - from: ">0.1.0", - description: - "Starting Jan 15, HTTP functions will be private by default. [Learn more](https://someurl.com)", - action: - "If you have not already done so during a previous update, after updating, you must switch your Cloud Scheduler jobs to PubSub, otherwise your extension will stop running.", - }, - ], - }, -}; - -const INSTANCE = { - name: "projects/invader-zim/instances/instance-of-official-ext", - createTime: "2019-05-19T00:20:10.416947Z", - updateTime: "2019-05-19T00:20:10.416947Z", - state: "ACTIVE", - config: { - name: - "projects/invader-zim/instances/instance-of-official-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", - createTime: "2019-05-19T00:20:10.416947Z", - sourceId: "fake-official-source", - sourceName: "projects/firebasemods/sources/fake-official-source", - source: { - name: "projects/firebasemods/sources/fake-official-source", - }, - }, -}; - -const REGISTRY_INSTANCE = { - name: "projects/invader-zim/instances/instance-of-registry-ext", - createTime: "2019-05-19T00:20:10.416947Z", - updateTime: "2019-05-19T00:20:10.416947Z", - state: "ACTIVE", - config: { - name: - "projects/invader-zim/instances/instance-of-registry-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", - createTime: "2019-05-19T00:20:10.416947Z", - sourceId: "fake-registry-source", - sourceName: "projects/firebasemods/sources/fake-registry-source", - extensionRef: "test-publisher/test", - source: { - name: "projects/firebasemods/sources/fake-registry-source", - }, - }, -}; - -const LOCAL_INSTANCE = { - name: "projects/invader-zim/instances/instance-of-local-ext", - createTime: "2019-05-19T00:20:10.416947Z", - updateTime: "2019-05-19T00:20:10.416947Z", - state: "ACTIVE", - config: { - name: - "projects/invader-zim/instances/instance-of-local-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", - createTime: "2019-05-19T00:20:10.416947Z", - sourceId: "fake-registry-source", - sourceName: "projects/firebasemods/sources/fake-local-source", - source: { - name: "projects/firebasemods/sources/fake-local-source", - }, - }, -}; - -describe("updateHelper", () => { - describe("updateFromLocalSource", () => { - let createSourceStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - createSourceStub = sinon.stub(extensionsHelper, "createSourceFromLocation"); - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(INSTANCE); - - // The logic will fetch the extensions registry, but it doesn't need to receive anything. - nock(firebaseExtensionsRegistryOrigin).get("/extensions.json").reply(200, {}); - }); - - afterEach(() => { - createSourceStub.restore(); - getInstanceStub.restore(); - - nock.cleanAll(); - }); - - it("should return the correct source name for a valid local source", async () => { - createSourceStub.resolves(SOURCE); - const name = await updateHelper.updateFromLocalSource( - "test-project", - "test-instance", - ".", - SPEC - ); - expect(name).to.equal(SOURCE.name); - }); - - it("should throw an error for an invalid source", async () => { - createSourceStub.throwsException("Invalid source"); - await expect( - updateHelper.updateFromLocalSource("test-project", "test-instance", ".", SPEC) - ).to.be.rejectedWith(FirebaseError, "Unable to update from the source"); - }); - }); - - describe("updateFromUrlSource", () => { - let createSourceStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - createSourceStub = sinon.stub(extensionsHelper, "createSourceFromLocation"); - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(INSTANCE); - - // The logic will fetch the extensions registry, but it doesn't need to receive anything. - nock(firebaseExtensionsRegistryOrigin).get("/extensions.json").reply(200, {}); - }); - - afterEach(() => { - createSourceStub.restore(); - getInstanceStub.restore(); - - nock.cleanAll(); - }); - - it("should return the correct source name for a valid url source", async () => { - createSourceStub.resolves(SOURCE); - const name = await updateHelper.updateFromUrlSource( - "test-project", - "test-instance", - "https://valid-source.tar.gz", - SPEC - ); - expect(name).to.equal(SOURCE.name); - }); - - it("should throw an error for an invalid source", async () => { - createSourceStub.throws("Invalid source"); - await expect( - updateHelper.updateFromUrlSource( - "test-project", - "test-instance", - "https://valid-source.tar.gz", - SPEC - ) - ).to.be.rejectedWith(FirebaseError, "Unable to update from the source"); - }); - }); - - describe("updateToVersionFromPublisherSource", () => { - let getExtensionStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let listExtensionVersionStub: sinon.SinonStub; - let registryStub: sinon.SinonStub; - let isOfficialStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - getExtensionStub = sinon.stub(extensionsApi, "getExtension"); - createSourceStub = sinon.stub(extensionsApi, "getExtensionVersion"); - listExtensionVersionStub = sinon.stub(extensionsApi, "listExtensionVersions"); - registryStub = sinon.stub(resolveSource, "resolveRegistryEntry"); - registryStub.resolves(REGISTRY_ENTRY); - isOfficialStub = sinon.stub(resolveSource, "isOfficialSource"); - isOfficialStub.returns(false); - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(REGISTRY_INSTANCE); - }); - - afterEach(() => { - getExtensionStub.restore(); - createSourceStub.restore(); - listExtensionVersionStub.restore(); - registryStub.restore(); - isOfficialStub.restore(); - getInstanceStub.restore(); - }); - - it("should return the correct source name for a valid published extension version source", async () => { - getExtensionStub.resolves(EXTENSION); - createSourceStub.resolves(EXTENSION_VERSION); - listExtensionVersionStub.resolves([]); - const name = await updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test@0.2.0", - SPEC - ); - expect(name).to.equal(EXTENSION_VERSION.name); - }); - - it("should throw an error for an invalid source", async () => { - getExtensionStub.throws(Error("NOT FOUND")); - createSourceStub.throws(Error("NOT FOUND")); - listExtensionVersionStub.resolves([]); - await expect( - updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test@1.2.3", - SPEC - ) - ).to.be.rejectedWith("NOT FOUND"); - }); - }); - - describe("updateFromPublisherSource", () => { - let getExtensionStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let listExtensionVersionStub: sinon.SinonStub; - let registryStub: sinon.SinonStub; - let isOfficialStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - getExtensionStub = sinon.stub(extensionsApi, "getExtension"); - createSourceStub = sinon.stub(extensionsApi, "getExtensionVersion"); - listExtensionVersionStub = sinon.stub(extensionsApi, "listExtensionVersions"); - registryStub = sinon.stub(resolveSource, "resolveRegistryEntry"); - registryStub.resolves(REGISTRY_ENTRY); - isOfficialStub = sinon.stub(resolveSource, "isOfficialSource"); - isOfficialStub.returns(false); - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(REGISTRY_INSTANCE); - }); - - afterEach(() => { - getExtensionStub.restore(); - createSourceStub.restore(); - listExtensionVersionStub.restore(); - registryStub.restore(); - isOfficialStub.restore(); - getInstanceStub.restore(); - }); - - it("should return the correct source name for the latest published extension source", async () => { - getExtensionStub.resolves(EXTENSION); - createSourceStub.resolves(EXTENSION_VERSION); - listExtensionVersionStub.resolves([]); - const name = await updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test", - SPEC - ); - expect(name).to.equal(EXTENSION_VERSION.name); - }); - - it("should throw an error for an invalid source", async () => { - getExtensionStub.throws(Error("NOT FOUND")); - createSourceStub.throws(Error("NOT FOUND")); - listExtensionVersionStub.resolves([]); - await expect( - updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test", - SPEC - ) - ).to.be.rejectedWith("NOT FOUND"); - }); - }); -}); - -describe("inferUpdateSource", () => { - it("should infer update source from ref without version", () => { - const result = updateHelper.inferUpdateSource("", "firebase/storage-resize-images"); - expect(result).to.equal("firebase/storage-resize-images@latest"); - }); - - it("should infer update source from ref with just version", () => { - const result = updateHelper.inferUpdateSource("0.1.2", "firebase/storage-resize-images"); - expect(result).to.equal("firebase/storage-resize-images@0.1.2"); - }); - - it("should infer update source from ref and extension name", () => { - const result = updateHelper.inferUpdateSource( - "storage-resize-images", - "firebase/storage-resize-images" - ); - expect(result).to.equal("firebase/storage-resize-images@latest"); - }); - - it("should infer update source if it is a ref distinct from the input ref", () => { - const result = updateHelper.inferUpdateSource( - "notfirebase/storage-resize-images", - "firebase/storage-resize-images" - ); - expect(result).to.equal("notfirebase/storage-resize-images@latest"); - }); -}); - -describe("getExistingSourceOrigin", () => { - let getInstanceStub: sinon.SinonStub; - - afterEach(() => { - getInstanceStub.restore(); - }); - - it("should return published extension as source origin", async () => { - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(REGISTRY_INSTANCE); - - const result = await updateHelper.getExistingSourceOrigin( - "invader-zim", - "instance-of-registry-ext", - "ext-testing", - "projects/firebasemods/sources/fake-registry-source" - ); - - expect(result).to.equal(extensionsHelper.SourceOrigin.PUBLISHED_EXTENSION); - }); - - it("should return local extension as source origin", async () => { - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(LOCAL_INSTANCE); - - const result = await updateHelper.getExistingSourceOrigin( - "invader-zim", - "instance-of-local-ext", - "ext-testing", - "projects/firebasemods/sources/fake-local-source" - ); - - expect(result).to.equal(extensionsHelper.SourceOrigin.LOCAL); - }); -}); diff --git a/src/test/extensions/utils.spec.ts b/src/test/extensions/utils.spec.ts deleted file mode 100644 index 4efecb6a04b..00000000000 --- a/src/test/extensions/utils.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expect } from "chai"; - -import * as utils from "../../extensions/utils"; - -describe("extensions utils", () => { - describe("formatTimestamp", () => { - it("should format timestamp correctly", () => { - expect(utils.formatTimestamp("2020-05-11T03:45:13.583677Z")).to.equal("2020-05-11 03:45:13"); - }); - }); -}); diff --git a/src/test/extensions/warnings.spec.ts b/src/test/extensions/warnings.spec.ts deleted file mode 100644 index d86a7d04275..00000000000 --- a/src/test/extensions/warnings.spec.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as resolveSource from "../../extensions/resolveSource"; -import * as utils from "../../utils"; -import * as warnings from "../../extensions/warnings"; -import { - Extension, - ExtensionVersion, - RegistryLaunchStage, - Visibility, -} from "../../extensions/extensionsApi"; -import { InstanceSpec } from "../../deploy/extensions/planner"; - -const testExtensionVersion: ExtensionVersion = { - name: "test", - ref: "test/test@0.1.0", - state: "PUBLISHED", - hash: "abc123", - sourceDownloadUri: "https://download.com/source", - spec: { - name: "test", - version: "0.1.0", - resources: [], - params: [], - sourceUrl: "github.com/test/meout", - }, -}; - -const testExtension = (publisherId: string, launchStage: RegistryLaunchStage): Extension => { - return { - name: "test", - ref: `${publisherId}/test`, - registryLaunchStage: launchStage, - createTime: "101", - visibility: Visibility.PUBLIC, - }; -}; - -const testInstanceSpec = ( - publisherId: string, - instanceId: string, - launchStage: RegistryLaunchStage -): InstanceSpec => { - return { - instanceId, - ref: { - publisherId, - extensionId: "test", - version: "0.1.0", - }, - params: {}, - extensionVersion: testExtensionVersion, - extension: testExtension(publisherId, launchStage), - }; -}; - -describe("displayWarningPrompts", () => { - let getTrustedPublisherStub: sinon.SinonStub; - let logLabeledStub: sinon.SinonStub; - - beforeEach(() => { - getTrustedPublisherStub = sinon.stub(resolveSource, "getTrustedPublishers"); - getTrustedPublisherStub.returns(["firebase"]); - logLabeledStub = sinon.stub(utils, "logLabeledBullet"); - }); - - afterEach(() => { - getTrustedPublisherStub.restore(); - logLabeledStub.restore(); - }); - - it("should not warn if from trusted publisher and not experimental", async () => { - const publisherId = "firebase"; - - await warnings.displayWarningPrompts( - publisherId, - RegistryLaunchStage.BETA, - testExtensionVersion - ); - - expect(logLabeledStub).to.not.have.been.called; - }); - - it("should warn if experimental", async () => { - const publisherId = "firebase"; - - await warnings.displayWarningPrompts( - publisherId, - RegistryLaunchStage.EXPERIMENTAL, - testExtensionVersion - ); - - expect(logLabeledStub).to.have.been.calledWithMatch("extensions", "experimental"); - }); - - it("should warn if the publisher is not on the approved publisher list", async () => { - const publisherId = "pubby-mcpublisher"; - - await warnings.displayWarningPrompts( - publisherId, - RegistryLaunchStage.BETA, - testExtensionVersion - ); - - expect(logLabeledStub).to.have.been.calledWithMatch("extensions", "Early Access Program"); - }); -}); - -describe("displayWarningsForDeploy", () => { - let getTrustedPublisherStub: sinon.SinonStub; - let logLabeledStub: sinon.SinonStub; - - beforeEach(() => { - getTrustedPublisherStub = sinon.stub(resolveSource, "getTrustedPublishers"); - getTrustedPublisherStub.returns(["firebase"]); - logLabeledStub = sinon.stub(utils, "logLabeledBullet"); - }); - - afterEach(() => { - getTrustedPublisherStub.restore(); - logLabeledStub.restore(); - }); - - it("should not warn or prompt if from trusted publisher and not experimental", async () => { - const toCreate = [ - testInstanceSpec("firebase", "ext-id-1", RegistryLaunchStage.GA), - testInstanceSpec("firebase", "ext-id-2", RegistryLaunchStage.GA), - ]; - - const warned = await warnings.displayWarningsForDeploy(toCreate); - - expect(warned).to.be.false; - expect(logLabeledStub).to.not.have.been.called; - }); - - it("should prompt if experimental", async () => { - const toCreate = [ - testInstanceSpec("firebase", "ext-id-1", RegistryLaunchStage.EXPERIMENTAL), - testInstanceSpec("firebase", "ext-id-2", RegistryLaunchStage.EXPERIMENTAL), - ]; - - const warned = await warnings.displayWarningsForDeploy(toCreate); - - expect(warned).to.be.true; - expect(logLabeledStub).to.have.been.calledWithMatch("extensions", "experimental"); - }); - - it("should prompt if the publisher is not on the approved publisher list", async () => { - const publisherId = "pubby-mcpublisher"; - - const toCreate = [ - testInstanceSpec("pubby-mcpublisher", "ext-id-1", RegistryLaunchStage.GA), - testInstanceSpec("pubby-mcpublisher", "ext-id-2", RegistryLaunchStage.GA), - ]; - - const warned = await warnings.displayWarningsForDeploy(toCreate); - - expect(warned).to.be.true; - expect(logLabeledStub).to.have.been.calledWithMatch("extensions", "Early Access Program"); - }); - - it("should show multiple warnings at once if triggered", async () => { - const publisherId = "pubby-mcpublisher"; - - const toCreate = [ - testInstanceSpec("pubby-mcpublisher", "ext-id-1", RegistryLaunchStage.GA), - testInstanceSpec("firebase", "ext-id-2", RegistryLaunchStage.EXPERIMENTAL), - ]; - - const warned = await warnings.displayWarningsForDeploy(toCreate); - - expect(warned).to.be.true; - expect(logLabeledStub).to.have.been.calledWithMatch("extensions", "Early Access Program"); - expect(logLabeledStub).to.have.been.calledWithMatch("extensions", "experimental"); - }); -}); diff --git a/src/test/firestore/indexes.spec.ts b/src/test/firestore/indexes.spec.ts deleted file mode 100644 index 8866678bfb2..00000000000 --- a/src/test/firestore/indexes.spec.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { expect } from "chai"; -import { FirestoreIndexes } from "../../firestore/indexes"; -import { FirebaseError } from "../../error"; -import * as API from "../../firestore/indexes-api"; -import * as Spec from "../../firestore/indexes-spec"; -import * as sort from "../../firestore/indexes-sort"; -import * as util from "../../firestore/util"; - -const idx = new FirestoreIndexes(); - -const VALID_SPEC = { - indexes: [ - { - collectionGroup: "collection", - queryScope: "COLLECTION", - fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "bar", order: "DESCENDING" }, - { fieldPath: "baz", arrayConfig: "CONTAINS" }, - ], - }, - ], - fieldOverrides: [ - { - collectionGroup: "collection", - fieldPath: "foo", - indexes: [ - { order: "ASCENDING", scope: "COLLECTION" }, - { arrayConfig: "CONTAINS", scope: "COLLECTION" }, - ], - }, - ], -}; - -describe("IndexValidation", () => { - it("should accept a valid v1beta2 index spec", () => { - idx.validateSpec(VALID_SPEC); - }); - - it("should not change a valid v1beta2 index spec after upgrade", () => { - const upgraded = idx.upgradeOldSpec(VALID_SPEC); - expect(upgraded).to.eql(VALID_SPEC); - }); - - it("should accept an empty spec", () => { - const empty = { - indexes: [], - }; - - idx.validateSpec(idx.upgradeOldSpec(empty)); - }); - - it("should accept a valid v1beta1 index spec after upgrade", () => { - idx.validateSpec( - idx.upgradeOldSpec({ - indexes: [ - { - collectionId: "collection", - fields: [ - { fieldPath: "foo", mode: "ASCENDING" }, - { fieldPath: "bar", mode: "DESCENDING" }, - { fieldPath: "baz", mode: "ARRAY_CONTAINS" }, - ], - }, - ], - }) - ); - }); - - it("should reject an incomplete index spec", () => { - expect(() => { - idx.validateSpec({ - indexes: [ - { - collectionGroup: "collection", - fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "bar", order: "DESCENDING" }, - ], - }, - ], - }); - }).to.throw(FirebaseError, /Must contain "queryScope"/); - }); - - it("should reject an overspecified index spec", () => { - expect(() => { - idx.validateSpec({ - indexes: [ - { - collectionGroup: "collection", - queryScope: "COLLECTION", - fields: [ - { fieldPath: "foo", order: "ASCENDING", arrayConfig: "CONTAINES" }, - { fieldPath: "bar", order: "DESCENDING" }, - ], - }, - ], - }); - }).to.throw(FirebaseError, /Must contain exactly one of "order,arrayConfig"/); - }); -}); - -describe("IndexNameParsing", () => { - it("should parse an index name correctly", () => { - const name = - "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123/"; - expect(util.parseIndexName(name)).to.eql({ - projectId: "myproject", - collectionGroupId: "collection", - indexId: "abc123", - }); - }); - - it("should parse a field name correctly", () => { - const name = - "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123/"; - expect(util.parseFieldName(name)).to.eql({ - projectId: "myproject", - collectionGroupId: "collection", - fieldPath: "abc123", - }); - }); -}); - -describe("IndexSpecMatching", () => { - it("should identify a positive index spec match", () => { - const apiIndex: API.Index = { - name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { fieldPath: "foo", order: API.Order.ASCENDING }, - { fieldPath: "bar", arrayConfig: API.ArrayConfig.CONTAINS }, - ], - state: API.State.READY, - }; - - const specIndex = { - collectionGroup: "collection", - queryScope: "COLLECTION", - fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "bar", arrayConfig: "CONTAINS" }, - ], - } as Spec.Index; - - expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(true); - }); - - it("should identify a negative index spec match", () => { - const apiIndex = { - name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", - queryScope: "COLLECTION", - fields: [ - { fieldPath: "foo", order: "DESCENDING" }, - { fieldPath: "bar", arrayConfig: "CONTAINS" }, - ], - state: API.State.READY, - } as API.Index; - - const specIndex = { - collectionGroup: "collection", - queryScope: "COLLECTION", - fields: [ - { fieldPath: "foo", order: "ASCENDING" }, - { fieldPath: "bar", arrayConfig: "CONTAINS" }, - ], - } as Spec.Index; - - // The second spec contains ASCENDING where the former contains DESCENDING - expect(idx.indexMatchesSpec(apiIndex, specIndex)).to.eql(false); - }); - - it("should identify a positive field spec match", () => { - const apiField = { - name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", - indexConfig: { - indexes: [ - { - queryScope: "COLLECTION", - fields: [{ fieldPath: "abc123", order: "ASCENDING" }], - }, - { - queryScope: "COLLECTION", - fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }], - }, - ], - }, - } as API.Field; - - const specField = { - collectionGroup: "collection", - fieldPath: "abc123", - indexes: [ - { order: "ASCENDING", queryScope: "COLLECTION" }, - { arrayConfig: "CONTAINS", queryScope: "COLLECTION" }, - ], - } as Spec.FieldOverride; - - expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); - }); - - it("should match a field spec with all indexes excluded", () => { - const apiField = { - name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", - indexConfig: {}, - } as API.Field; - - const specField = { - collectionGroup: "collection", - fieldPath: "abc123", - indexes: [], - } as Spec.FieldOverride; - - expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true); - }); - - it("should identify a negative field spec match", () => { - const apiField = { - name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123", - indexConfig: { - indexes: [ - { - queryScope: "COLLECTION", - fields: [{ fieldPath: "abc123", order: "ASCENDING" }], - }, - { - queryScope: "COLLECTION", - fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }], - }, - ], - }, - } as API.Field; - - const specField = { - collectionGroup: "collection", - fieldPath: "abc123", - indexes: [ - { order: "DESCENDING", queryScope: "COLLECTION" }, - { arrayConfig: "CONTAINS", queryScope: "COLLECTION" }, - ], - } as Spec.FieldOverride; - - // The second spec contains "DESCENDING" where the first contains "ASCENDING" - expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false); - }); -}); - -describe("IndexSorting", () => { - it("should be able to handle empty arrays", () => { - expect(([] as Spec.Index[]).sort(sort.compareSpecIndex)).to.eql([]); - expect(([] as Spec.FieldOverride[]).sort(sort.compareFieldOverride)).to.eql([]); - expect(([] as API.Index[]).sort(sort.compareApiIndex)).to.eql([]); - expect(([] as API.Field[]).sort(sort.compareApiField)).to.eql([]); - }); - - it("should correctly sort an array of Spec indexes", () => { - // Sorts first because of collectionGroup - const a: Spec.Index = { - collectionGroup: "collectionA", - queryScope: API.QueryScope.COLLECTION, - fields: [], - }; - - // fieldA ASCENDING should sort before fieldA DESCENDING - const b: Spec.Index = { - collectionGroup: "collectionB", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { - fieldPath: "fieldA", - order: API.Order.ASCENDING, - }, - ], - }; - - // This compound index sorts before the following simple - // index because the first element sorts first. - const c: Spec.Index = { - collectionGroup: "collectionB", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { - fieldPath: "fieldA", - order: API.Order.ASCENDING, - }, - { - fieldPath: "fieldB", - order: API.Order.ASCENDING, - }, - ], - }; - - const d: Spec.Index = { - collectionGroup: "collectionB", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { - fieldPath: "fieldB", - order: API.Order.ASCENDING, - }, - ], - }; - - const e: Spec.Index = { - collectionGroup: "collectionB", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { - fieldPath: "fieldB", - order: API.Order.ASCENDING, - }, - { - fieldPath: "fieldA", - order: API.Order.ASCENDING, - }, - ], - }; - - expect([b, a, e, d, c].sort(sort.compareSpecIndex)).to.eql([a, b, c, d, e]); - }); - - it("should correcty sort an array of Spec field overrides", () => { - // Sorts first because of collectionGroup - const a: Spec.FieldOverride = { - collectionGroup: "collectionA", - fieldPath: "fieldA", - indexes: [], - }; - - const b: Spec.FieldOverride = { - collectionGroup: "collectionB", - fieldPath: "fieldA", - indexes: [], - }; - - // Order indexes sort before Array indexes - const c: Spec.FieldOverride = { - collectionGroup: "collectionB", - fieldPath: "fieldB", - indexes: [ - { - queryScope: API.QueryScope.COLLECTION, - order: API.Order.ASCENDING, - }, - ], - }; - - const d: Spec.FieldOverride = { - collectionGroup: "collectionB", - fieldPath: "fieldB", - indexes: [ - { - queryScope: API.QueryScope.COLLECTION, - arrayConfig: API.ArrayConfig.CONTAINS, - }, - ], - }; - - expect([b, a, d, c].sort(sort.compareFieldOverride)).to.eql([a, b, c, d]); - }); - - it("should correctly sort an array of API indexes", () => { - // Sorts first because of collectionGroup - const a: API.Index = { - name: "/projects/project/databases/(default)/collectionGroups/collectionA/indexes/a", - queryScope: API.QueryScope.COLLECTION, - fields: [], - }; - - // fieldA ASCENDING should sort before fieldA DESCENDING - const b: API.Index = { - name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/b", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { - fieldPath: "fieldA", - order: API.Order.ASCENDING, - }, - ], - }; - - // This compound index sorts before the following simple - // index because the first element sorts first. - const c: API.Index = { - name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/c", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { - fieldPath: "fieldA", - order: API.Order.ASCENDING, - }, - { - fieldPath: "fieldB", - order: API.Order.ASCENDING, - }, - ], - }; - - const d: API.Index = { - name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/d", - queryScope: API.QueryScope.COLLECTION, - fields: [ - { - fieldPath: "fieldA", - order: API.Order.DESCENDING, - }, - ], - }; - - expect([b, a, d, c].sort(sort.compareApiIndex)).to.eql([a, b, c, d]); - }); - - it("should correctly sort an array of API field overrides", () => { - // Sorts first because of collectionGroup - const a: API.Field = { - name: "/projects/myproject/databases/(default)/collectionGroups/collectionA/fields/fieldA", - indexConfig: { - indexes: [], - }, - }; - - const b: API.Field = { - name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldA", - indexConfig: { - indexes: [], - }, - }; - - // Order indexes sort before Array indexes - const c: API.Field = { - name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldB", - indexConfig: { - indexes: [ - { - queryScope: API.QueryScope.COLLECTION, - fields: [{ fieldPath: "fieldB", order: API.Order.DESCENDING }], - }, - ], - }, - }; - - const d: API.Field = { - name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldB", - indexConfig: { - indexes: [ - { - queryScope: API.QueryScope.COLLECTION, - fields: [{ fieldPath: "fieldB", arrayConfig: API.ArrayConfig.CONTAINS }], - }, - ], - }, - }; - - expect([b, a, d, c].sort(sort.compareApiField)).to.eql([a, b, c, d]); - }); -}); diff --git a/src/test/fixtures/config-imports/hosting.json b/src/test/fixtures/config-imports/hosting.json index a197ccc4273..895f9255178 100644 --- a/src/test/fixtures/config-imports/hosting.json +++ b/src/test/fixtures/config-imports/hosting.json @@ -1,6 +1,6 @@ { // this is a comment, deal with it "public": ".", - "ignore": ["**/.*"], + "ignore": ["index.ts", "**/.*"], "extra": true } diff --git a/src/test/fixtures/config-imports/index.ts b/src/test/fixtures/config-imports/index.ts new file mode 100644 index 00000000000..a3413be33bb --- /dev/null +++ b/src/test/fixtures/config-imports/index.ts @@ -0,0 +1,8 @@ +import { resolve } from "path"; + +/** + * A directory containing a simple firebase.json with hosting and rules config. + */ +export const FIXTURE_DIR = __dirname; + +export const FIREBASE_JSON_PATH = resolve(__dirname, "firebase.json"); diff --git a/src/test/fixtures/dup-top-level/index.ts b/src/test/fixtures/dup-top-level/index.ts new file mode 100644 index 00000000000..34535ab3187 --- /dev/null +++ b/src/test/fixtures/dup-top-level/index.ts @@ -0,0 +1,5 @@ +/** + * A directory containing a rules.json that has a top-level `rules: {...}` key + * that is duplicate with the `rules:` key in `firebase.json`. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/extension-yamls/hello-world/extension.yaml b/src/test/fixtures/extension-yamls/hello-world/extension.yaml new file mode 100644 index 00000000000..7ebd008f641 --- /dev/null +++ b/src/test/fixtures/extension-yamls/hello-world/extension.yaml @@ -0,0 +1,61 @@ +# Learn detailed information about the fields of an extension.yaml file in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml + +# Identifier for your extension +# TODO: Replace this with an descriptive name for your extension. +name: greet-the-world +version: 0.0.1 # Follow semver versioning +specVersion: v1beta # Version of the Firebase Extensions specification + +# Friendly display name for your extension (~3-5 words) +displayName: Greet the world + +# Brief description of the task your extension performs (~1 sentence) +description: >- + Sends the world a greeting. + +license: Apache-2.0 # https://spdx.org/licenses/ + +# Public URL for the source code of your extension. +# TODO: Replace this with your GitHub repo. +sourceUrl: https://github.com/ORG_OR_USER/REPO_NAME + +# Specify whether a paid-tier billing plan is required to use your extension. +# Learn more in the docs: https://firebase.google.com/docs/extensions/reference/extension-yaml#billing-required-field +billingRequired: true + +# In an `apis` field, list any Google APIs (like Cloud Translation, BigQuery, etc.) +# required for your extension to operate. +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#apis-field + +# In a `roles` field, list any IAM access roles required for your extension to operate. +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#roles-field + +# In the `resources` field, list each of your extension's functions, including the trigger for each function. +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#resources-field +resources: + - name: greetTheWorld + type: firebaseextensions.v1beta.function + description: >- + HTTP request-triggered function that responds with a specified greeting message + properties: + # httpsTrigger is used for an HTTP triggered function. + httpsTrigger: {} + runtime: "nodejs16" + +# In the `params` field, set up your extension's user-configured parameters. +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#params-field +params: + - param: GREETING + label: Greeting for the world + description: >- + What do you want to say to the world? + For example, Hello world? or What's up, world? + type: string + default: Hello + required: true + immutable: false diff --git a/src/test/fixtures/extension-yamls/hello-world/index.ts b/src/test/fixtures/extension-yamls/hello-world/index.ts new file mode 100644 index 00000000000..76a3b547326 --- /dev/null +++ b/src/test/fixtures/extension-yamls/hello-world/index.ts @@ -0,0 +1,4 @@ +/** + * A valid extension directory containing a full-blown extension.yaml. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/extension-yamls/invalid/extension.yaml b/src/test/fixtures/extension-yamls/invalid/extension.yaml new file mode 100644 index 00000000000..fe8248214ba --- /dev/null +++ b/src/test/fixtures/extension-yamls/invalid/extension.yaml @@ -0,0 +1,3 @@ +name: foo +unknownkey +other: value diff --git a/src/test/fixtures/extension-yamls/invalid/index.ts b/src/test/fixtures/extension-yamls/invalid/index.ts new file mode 100644 index 00000000000..330fb76e33f --- /dev/null +++ b/src/test/fixtures/extension-yamls/invalid/index.ts @@ -0,0 +1,4 @@ +/** + * An extension directory containing an invalid extension.yaml. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/extension-yamls/minimal/extension.yaml b/src/test/fixtures/extension-yamls/minimal/extension.yaml new file mode 100644 index 00000000000..6cdde1230c1 --- /dev/null +++ b/src/test/fixtures/extension-yamls/minimal/extension.yaml @@ -0,0 +1,17 @@ +# Learn detailed information about the fields of an extension.yaml file in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml + +# Identifier for your extension +# TODO: Replace this with an descriptive name for your extension. +name: greet-the-world +version: 0.0.1 # Follow semver versioning +specVersion: v1beta # Version of the Firebase Extensions specification + +# Friendly display name for your extension (~3-5 words) +displayName: Greet the world + +# Brief description of the task your extension performs (~1 sentence) +description: >- + Sends the world a greeting. + +license: Apache-2.0 # https://spdx.org/licenses/ diff --git a/src/test/fixtures/extension-yamls/minimal/index.ts b/src/test/fixtures/extension-yamls/minimal/index.ts new file mode 100644 index 00000000000..129edd00e7f --- /dev/null +++ b/src/test/fixtures/extension-yamls/minimal/index.ts @@ -0,0 +1,4 @@ +/** + * A valid extension directory containing a minimal extension.yaml. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/sample-ext-preinstall/PREINSTALL.md b/src/test/fixtures/extension-yamls/sample-ext-preinstall/PREINSTALL.md similarity index 100% rename from src/test/fixtures/sample-ext-preinstall/PREINSTALL.md rename to src/test/fixtures/extension-yamls/sample-ext-preinstall/PREINSTALL.md diff --git a/src/test/fixtures/sample-ext-preinstall/extension.yaml b/src/test/fixtures/extension-yamls/sample-ext-preinstall/extension.yaml similarity index 100% rename from src/test/fixtures/sample-ext-preinstall/extension.yaml rename to src/test/fixtures/extension-yamls/sample-ext-preinstall/extension.yaml diff --git a/src/test/fixtures/extension-yamls/sample-ext-preinstall/index.ts b/src/test/fixtures/extension-yamls/sample-ext-preinstall/index.ts new file mode 100644 index 00000000000..31b33794368 --- /dev/null +++ b/src/test/fixtures/extension-yamls/sample-ext-preinstall/index.ts @@ -0,0 +1,4 @@ +/** + * A valid extension directory containing a PREINSTALL.md. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/sample-ext/extension.yaml b/src/test/fixtures/extension-yamls/sample-ext/extension.yaml similarity index 100% rename from src/test/fixtures/sample-ext/extension.yaml rename to src/test/fixtures/extension-yamls/sample-ext/extension.yaml diff --git a/src/test/fixtures/extension-yamls/sample-ext/index.ts b/src/test/fixtures/extension-yamls/sample-ext/index.ts new file mode 100644 index 00000000000..960de77a15a --- /dev/null +++ b/src/test/fixtures/extension-yamls/sample-ext/index.ts @@ -0,0 +1,4 @@ +/** + * A valid extension directory containing an extension.yaml. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/fbrc/index.ts b/src/test/fixtures/fbrc/index.ts new file mode 100644 index 00000000000..c1fc56f018e --- /dev/null +++ b/src/test/fixtures/fbrc/index.ts @@ -0,0 +1,23 @@ +import { resolve } from "path"; + +/** + * A directory containing a valid .firebaserc file along firebase.json. + */ +export const VALID_RC_DIR = __dirname; + +/** + * Path of the firebase.json in the `VALID_RC_DIR` directory. + */ +export const FIREBASE_JSON_PATH = resolve(__dirname, "firebase.json"); + +/** + * A directory containing a .firebaserc file containing invalid JSON. + */ +export const INVALID_RC_DIR = resolve(__dirname, "invalid"); + +/** + * A directory containing a .firebaserc file with project alias conflicts. + * + * While it does not contain a firebase.json, its parent directory does. + */ +export const CONFLICT_RC_DIR = resolve(__dirname, "conflict"); diff --git a/src/test/fixtures/ignores/firebase.json b/src/test/fixtures/ignores/firebase.json index beaea955c49..d6a28d7056e 100644 --- a/src/test/fixtures/ignores/firebase.json +++ b/src/test/fixtures/ignores/firebase.json @@ -1,9 +1,6 @@ { "hosting": { "public": ".", - "ignore": [ - "ignored.txt", - "ignored/**/*.txt" - ] + "ignore": ["index.ts", "ignored.txt", "ignored/**/*.txt"] } } diff --git a/src/test/fixtures/ignores/index.ts b/src/test/fixtures/ignores/index.ts new file mode 100644 index 00000000000..6a861da44fc --- /dev/null +++ b/src/test/fixtures/ignores/index.ts @@ -0,0 +1,4 @@ +/** + * A directory containing a firebase.json that specifies files to be ignored. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/invalid-config/firebase.json b/src/test/fixtures/invalid-config/firebase.json deleted file mode 100644 index 4cb70803e19..00000000000 --- a/src/test/fixtures/invalid-config/firebase.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "firebase": "myfirebase", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rules": "config/security-rules.json", - "redirects": [ { - "source" : "/foo", - "destination" : "/bar", - "type" : 301 - }, { - "source" : "/firebase/*", - "destination" : "https://firebase.google.com", - "type" : 302 - } ], - "rewrites": [ { - "source": "**", - "destination": "/index.html" - } ], - "headers": [ { - "source" : "**/*.@(eot|otf|ttf|ttc|woff|font.css)", - "headers" : [ { - "key" : "Access-Control-Allow-Origin", - "value" : "*" - } ] - }, { - "source" : "**/*.@(jpg|jpeg|gif|png)", - "headers" : [ { - "key" : "Cache-Control", - "value" : "max-age=7200" - } ] - }, { - "source" : "404.html", - "headers" : [ { - "key" : "Cache-Control", - "value" : "max-age=300" - } ] - } ] -} diff --git a/src/test/fixtures/profiler-data/index.ts b/src/test/fixtures/profiler-data/index.ts new file mode 100644 index 00000000000..0df271d2654 --- /dev/null +++ b/src/test/fixtures/profiler-data/index.ts @@ -0,0 +1,11 @@ +import { resolve } from "path"; + +/** + * A sample JSON file for profiler input. + */ +export const SAMPLE_INPUT_PATH = resolve(__dirname, "sample.json"); + +/** + * A sample JSON output file generated by the profiler. + */ +export const SAMPLE_OUTPUT_PATH = resolve(__dirname, "sample-output.json"); diff --git a/src/test/fixtures/rulesDeploy/index.ts b/src/test/fixtures/rulesDeploy/index.ts new file mode 100644 index 00000000000..6c3d3c29228 --- /dev/null +++ b/src/test/fixtures/rulesDeploy/index.ts @@ -0,0 +1,8 @@ +import { resolve } from "path"; + +/** + * A directory containing firestore and storage rules to be deployed. + */ +export const FIXTURE_DIR = __dirname; + +export const FIXTURE_FIRESTORE_RULES_PATH = resolve(__dirname, "firestore.rules"); diff --git a/src/test/fixtures/rulesDeployCrossService/firebase.json b/src/test/fixtures/rulesDeployCrossService/firebase.json new file mode 100644 index 00000000000..5d6165d2119 --- /dev/null +++ b/src/test/fixtures/rulesDeployCrossService/firebase.json @@ -0,0 +1,9 @@ +{ + "storage": { + "rules": "storage.rules" + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + } +} \ No newline at end of file diff --git a/src/test/fixtures/rulesDeployCrossService/index.ts b/src/test/fixtures/rulesDeployCrossService/index.ts new file mode 100644 index 00000000000..972e0327750 --- /dev/null +++ b/src/test/fixtures/rulesDeployCrossService/index.ts @@ -0,0 +1,4 @@ +/** + * A directory containing storage rules that fetches data from Firestore. + */ +export const FIXTURE_DIR = __dirname; diff --git a/src/test/fixtures/rulesDeployCrossService/storage.rules b/src/test/fixtures/rulesDeployCrossService/storage.rules new file mode 100644 index 00000000000..9fe5aa7feea --- /dev/null +++ b/src/test/fixtures/rulesDeployCrossService/storage.rules @@ -0,0 +1,7 @@ +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if firestore.exists(/databases/default/documents/foo/bar); + } + } +} \ No newline at end of file diff --git a/src/test/fixtures/simplehosting/index.ts b/src/test/fixtures/simplehosting/index.ts new file mode 100644 index 00000000000..bd82583f030 --- /dev/null +++ b/src/test/fixtures/simplehosting/index.ts @@ -0,0 +1,8 @@ +import { resolve } from "path"; + +/** + * A directory containing a simple project with Firebase Hosting configured. + */ +export const FIXTURE_DIR = __dirname; + +export const FIREBASE_JSON_PATH = resolve(__dirname, "firebase.json"); diff --git a/src/test/fixtures/valid-config/firebase.json b/src/test/fixtures/valid-config/firebase.json index 3122016789d..9d34cd25c9c 100644 --- a/src/test/fixtures/valid-config/firebase.json +++ b/src/test/fixtures/valid-config/firebase.json @@ -17,6 +17,13 @@ "type" : 302 } ], "rewrites": [ { + "source": "/region", + "function": "/foobar", + "region": "us-central1" + }, { + "source": "/default", + "function": "/foobar" + }, { "source": "**", "destination": "/index.html" } ], diff --git a/src/test/fixtures/valid-config/index.ts b/src/test/fixtures/valid-config/index.ts new file mode 100644 index 00000000000..c92933daaff --- /dev/null +++ b/src/test/fixtures/valid-config/index.ts @@ -0,0 +1,8 @@ +import { resolve } from "path"; + +/** + * A directory containing valid full-blown firebase.json. + */ +export const FIXTURE_DIR = __dirname; + +export const FIREBASE_JSON_PATH = resolve(__dirname, "firebase.json"); diff --git a/src/test/fixtures/zip-files/index.ts b/src/test/fixtures/zip-files/index.ts new file mode 100644 index 00000000000..b0912fa25b8 --- /dev/null +++ b/src/test/fixtures/zip-files/index.ts @@ -0,0 +1,18 @@ +import { join } from "path"; + +const zipFixturesDir = __dirname; +const testDataDir = join(zipFixturesDir, "node-unzipper-testData"); + +export const ZIP_CASES = [ + "compressed-cp866", + "compressed-directory-entry", + "compressed-flags-set", + "compressed-standard", + "uncompressed", + "zip-slip", + "zip64", +].map((name) => ({ + name, + archivePath: join(testDataDir, name, "archive.zip"), + inflatedDir: join(testDataDir, name, "inflated"), +})); diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-cp866/archive.zip b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-cp866/archive.zip new file mode 100644 index 00000000000..04bd3c9372a Binary files /dev/null and b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-cp866/archive.zip differ diff --git "a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-cp866/inflated/\320\242\320\265\321\201\321\202.txt" "b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-cp866/inflated/\320\242\320\265\321\201\321\202.txt" new file mode 100644 index 00000000000..2f29f70d4d5 --- /dev/null +++ "b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-cp866/inflated/\320\242\320\265\321\201\321\202.txt" @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/archive.zip b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/archive.zip new file mode 100644 index 00000000000..e81a6aa7e06 Binary files /dev/null and b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/archive.zip differ diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/META-INF/container.xml b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/META-INF/container.xml new file mode 100644 index 00000000000..f17cad9aeb0 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/META-INF/container.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/content.opf b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/content.opf new file mode 100644 index 00000000000..d3ff63669bb --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/content.opf @@ -0,0 +1,22 @@ + + + + calibre (3.15.0) [https://calibre-ebook.com] + 2006-03-06T20:06:33+00:00 + 8ee1add8-e31f-4b26-8059-e939a3190706 + en + Author text + Title text + + + + + + + + + + + + + diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/index.html b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/index.html new file mode 100644 index 00000000000..7ddea554403 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/index.html @@ -0,0 +1,15 @@ + + + + Blank PDF Document + + + + + + + + +

    + + diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/mimetype b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/mimetype new file mode 100644 index 00000000000..57ef03f24a4 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/mimetype @@ -0,0 +1 @@ +application/epub+zip \ No newline at end of file diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/page_styles.css b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/page_styles.css new file mode 100644 index 00000000000..7ee6c2c84a7 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/page_styles.css @@ -0,0 +1,4 @@ +@page { + margin-bottom: 5pt; + margin-top: 5pt + } diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/stylesheet.css b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/stylesheet.css new file mode 100644 index 00000000000..749a0321bb9 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/stylesheet.css @@ -0,0 +1,11 @@ +.calibre { + display: block; + font-size: 1em; + padding-left: 0; + padding-right: 0; + margin: 0 5pt + } +.calibre1 { + display: block; + margin: 1em 0 + } diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/toc.ncx b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/toc.ncx new file mode 100644 index 00000000000..80db2a6c5f2 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-directory-entry/inflated/toc.ncx @@ -0,0 +1,21 @@ + + + + + + + + + + + Title text + + + + + Start + + + + + diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/archive.zip b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/archive.zip new file mode 100644 index 00000000000..015ce233c46 Binary files /dev/null and b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/archive.zip differ diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/inflated/dir/fileInsideDir.txt b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/inflated/dir/fileInsideDir.txt new file mode 100644 index 00000000000..d81cc0710eb --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/inflated/dir/fileInsideDir.txt @@ -0,0 +1 @@ +42 diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/inflated/file.txt b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/inflated/file.txt new file mode 100644 index 00000000000..ac652242866 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-flags-set/inflated/file.txt @@ -0,0 +1,11 @@ +node.js rocks + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras commodo molestie nunc, eu pharetra libero accumsan nec. Vestibulum hendrerit, augue ac congue varius, enim metus congue quam, imperdiet gravida diam felis nec dui. Morbi ipsum enim, tristique nec congue a, commodo ac sapien. Praesent semper metus quis diam hendrerit ut condimentum eros lobortis. Aenean faucibus arcu nec leo aliquam tincidunt. Nunc bibendum dictum bibendum. Nunc ultricies pretium lacus, sit amet lobortis quam egestas quis. Fusce viverra magna rhoncus sem posuere non tempus nulla vestibulum. + +Sed aliquet, odio vel condimentum pellentesque, mauris risus iaculis elit, at congue erat mi at ante. In at dictum metus. Ut rutrum mauris felis. Nulla sed risus nunc, eget ultrices est. Nullam gravida diam in arcu vulputate varius. Sed id egestas magna. Ut a libero sapien. + +Integer congue felis ut nisl fringilla ac interdum est pretium. Proin tellus augue, molestie id ultricies placerat, ornare a felis. In eu nibh velit. Pellentesque cursus ultricies fermentum. Mauris eget velit tempor nulla bibendum accumsan sit amet a ante. Morbi rutrum tempor varius. Aenean congue leo vitae mi suscipit ac tempor nibh pulvinar. Maecenas risus eros, sodales quis tincidunt non, vulputate eget orci. Maecenas condimentum lectus pretium orci adipiscing interdum. Sed interdum vehicula urna ut scelerisque. + +Phasellus pellentesque tellus in neque auctor pellentesque adipiscing justo consequat. In tincidunt rhoncus mollis. Suspendisse quis est elit, vel semper lorem. Donec cursus, leo ac fermentum luctus, dui dolor pretium nunc, vel congue eros arcu sit amet enim. Nam nibh orci, laoreet id volutpat eu, aliquet sed ligula. Donec placerat sagittis leo, eget hendrerit nisi varius sed. In pharetra erat non justo interdum id tempus purus tempor. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse felis leo, pellentesque tristique iaculis consequat, vestibulum a erat. Curabitur ligula risus, consectetur at adipiscing sit amet, accumsan non justo. Proin ultricies molestie lorem et auctor. Duis commodo varius semper. Ut tempus porttitor dolor nec mattis. Cras massa eros, tincidunt eget placerat a, luctus eu arcu. Nulla ac orci vitae odio dapibus dictum vitae porta erat. + +Duis luctus convallis euismod. Integer orci massa, bibendum eu blandit quis, facilisis lobortis purus. Donec et sapien quis elit fermentum cursus a ut lacus. Nullam tellus felis, congue et pulvinar sit amet, luctus ac augue. Sed massa nunc, dignissim non viverra ac, dictum sit amet erat. Sed nunc tortor, convallis et tristique ut, aliquam ut orci. Integer nec magna vitae elit sagittis accumsan id ac mi. diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/archive.zip b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/archive.zip new file mode 100644 index 00000000000..327aab67163 Binary files /dev/null and b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/archive.zip differ diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/inflated/dir/fileInsideDir.txt b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/inflated/dir/fileInsideDir.txt new file mode 100644 index 00000000000..d81cc0710eb --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/inflated/dir/fileInsideDir.txt @@ -0,0 +1 @@ +42 diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/inflated/file.txt b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/inflated/file.txt new file mode 100644 index 00000000000..ac652242866 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/compressed-standard/inflated/file.txt @@ -0,0 +1,11 @@ +node.js rocks + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras commodo molestie nunc, eu pharetra libero accumsan nec. Vestibulum hendrerit, augue ac congue varius, enim metus congue quam, imperdiet gravida diam felis nec dui. Morbi ipsum enim, tristique nec congue a, commodo ac sapien. Praesent semper metus quis diam hendrerit ut condimentum eros lobortis. Aenean faucibus arcu nec leo aliquam tincidunt. Nunc bibendum dictum bibendum. Nunc ultricies pretium lacus, sit amet lobortis quam egestas quis. Fusce viverra magna rhoncus sem posuere non tempus nulla vestibulum. + +Sed aliquet, odio vel condimentum pellentesque, mauris risus iaculis elit, at congue erat mi at ante. In at dictum metus. Ut rutrum mauris felis. Nulla sed risus nunc, eget ultrices est. Nullam gravida diam in arcu vulputate varius. Sed id egestas magna. Ut a libero sapien. + +Integer congue felis ut nisl fringilla ac interdum est pretium. Proin tellus augue, molestie id ultricies placerat, ornare a felis. In eu nibh velit. Pellentesque cursus ultricies fermentum. Mauris eget velit tempor nulla bibendum accumsan sit amet a ante. Morbi rutrum tempor varius. Aenean congue leo vitae mi suscipit ac tempor nibh pulvinar. Maecenas risus eros, sodales quis tincidunt non, vulputate eget orci. Maecenas condimentum lectus pretium orci adipiscing interdum. Sed interdum vehicula urna ut scelerisque. + +Phasellus pellentesque tellus in neque auctor pellentesque adipiscing justo consequat. In tincidunt rhoncus mollis. Suspendisse quis est elit, vel semper lorem. Donec cursus, leo ac fermentum luctus, dui dolor pretium nunc, vel congue eros arcu sit amet enim. Nam nibh orci, laoreet id volutpat eu, aliquet sed ligula. Donec placerat sagittis leo, eget hendrerit nisi varius sed. In pharetra erat non justo interdum id tempus purus tempor. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse felis leo, pellentesque tristique iaculis consequat, vestibulum a erat. Curabitur ligula risus, consectetur at adipiscing sit amet, accumsan non justo. Proin ultricies molestie lorem et auctor. Duis commodo varius semper. Ut tempus porttitor dolor nec mattis. Cras massa eros, tincidunt eget placerat a, luctus eu arcu. Nulla ac orci vitae odio dapibus dictum vitae porta erat. + +Duis luctus convallis euismod. Integer orci massa, bibendum eu blandit quis, facilisis lobortis purus. Donec et sapien quis elit fermentum cursus a ut lacus. Nullam tellus felis, congue et pulvinar sit amet, luctus ac augue. Sed massa nunc, dignissim non viverra ac, dictum sit amet erat. Sed nunc tortor, convallis et tristique ut, aliquam ut orci. Integer nec magna vitae elit sagittis accumsan id ac mi. diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/archive.zip b/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/archive.zip new file mode 100644 index 00000000000..2d3626d6b6f Binary files /dev/null and b/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/archive.zip differ diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/inflated/dir/fileInsideDir.txt b/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/inflated/dir/fileInsideDir.txt new file mode 100644 index 00000000000..d81cc0710eb --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/inflated/dir/fileInsideDir.txt @@ -0,0 +1 @@ +42 diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/inflated/file.txt b/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/inflated/file.txt new file mode 100644 index 00000000000..210e1e1b832 --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/uncompressed/inflated/file.txt @@ -0,0 +1 @@ +node.js rocks diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/zip-slip/archive.zip b/src/test/fixtures/zip-files/node-unzipper-testData/zip-slip/archive.zip new file mode 100644 index 00000000000..38b3f499de0 Binary files /dev/null and b/src/test/fixtures/zip-files/node-unzipper-testData/zip-slip/archive.zip differ diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/zip-slip/inflated/good.txt b/src/test/fixtures/zip-files/node-unzipper-testData/zip-slip/inflated/good.txt new file mode 100644 index 00000000000..717599845fd --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/zip-slip/inflated/good.txt @@ -0,0 +1 @@ +this is a good one diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/zip64/archive.zip b/src/test/fixtures/zip-files/node-unzipper-testData/zip64/archive.zip new file mode 100644 index 00000000000..a2ee1fa33dc Binary files /dev/null and b/src/test/fixtures/zip-files/node-unzipper-testData/zip64/archive.zip differ diff --git a/src/test/fixtures/zip-files/node-unzipper-testData/zip64/inflated/README b/src/test/fixtures/zip-files/node-unzipper-testData/zip64/inflated/README new file mode 100644 index 00000000000..ba4fa995f7f --- /dev/null +++ b/src/test/fixtures/zip-files/node-unzipper-testData/zip64/inflated/README @@ -0,0 +1 @@ +This small file is in ZIP64 format. diff --git a/src/test/functional.spec.ts b/src/test/functional.spec.ts deleted file mode 100644 index 976a7d14e5a..00000000000 --- a/src/test/functional.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { expect } from "chai"; -import { flatten } from "lodash"; - -import * as f from "../functional"; - -describe("functional", () => { - describe("flatten", () => { - it("can iterate an empty object", () => { - expect([...f.flatten({})]).to.deep.equal([]); - }); - - it("can iterate an object that's already flat", () => { - expect([...f.flatten({ a: "b" })]).to.deep.equal([["a", "b"]]); - }); - - it("can handle nested objects", () => { - const init = { - outer: { - inner: { - value: 42, - }, - }, - other: { - value: null, - }, - }; - - const expected = [ - ["outer.inner.value", 42], - ["other.value", null], - ]; - - expect([...f.flatten(init)]).to.deep.equal(expected); - }); - - it("can handle objects with array values", () => { - const init = { values: ["a", "b"] }; - const expected = [ - ["values.0", "a"], - ["values.1", "b"], - ]; - - expect([...f.flatten(init)]).to.deep.equal(expected); - }); - - it("can iterate an empty array", () => { - expect([...flatten([])]).to.deep.equal([]); - }); - - it("can noop arrays", () => { - const init = ["a", "b", "c"]; - expect([...f.flatten(init)]).to.deep.equal(init); - }); - - it("can flatten", () => { - const init = [[[1]], [2], 3]; - expect([...f.flatten(init)]).to.deep.equal([1, 2, 3]); - }); - }); - - describe("reduceFlat", () => { - it("can noop", () => { - const init = ["a", "b", "c"]; - expect(init.reduce(f.reduceFlat, [])).to.deep.equal(["a", "b", "c"]); - }); - - it("can flatten", () => { - const init = [[[1]], [2], 3]; - expect(init.reduce(f.reduceFlat, [])).to.deep.equal([1, 2, 3]); - }); - }); - - describe("zip", () => { - it("can handle an empty array", () => { - expect([...f.zip([], [])]).to.deep.equal([]); - }); - it("can zip", () => { - expect([...f.zip([1], ["a"])]).to.deep.equal([[1, "a"]]); - }); - it("throws on length mismatch", () => { - expect(() => f.zip([1], [])).to.throw; - }); - }); - - it("zipIn", () => { - expect([1, 2].map(f.zipIn(["a", "b"]))).to.deep.equal([ - [1, "a"], - [2, "b"], - ]); - }); - - it("assertExhaustive", () => { - interface Bird { - type: "bird"; - } - interface Fish { - type: "fish"; - } - type Animal = Bird | Fish; - - // eslint-disable-next-line - function passtime(animal: Animal): string { - if (animal.type === "bird") { - return "fly"; - } else if (animal.type === "fish") { - return "swim"; - } - - // This line must make the containing function compile: - f.assertExhaustive(animal); - } - - // eslint-disable-next-line - function speak(animal: Animal): void { - if (animal.type === "bird") { - console.log("chirp"); - return; - } - // This line must cause the containing function to fail - // compilation if uncommented - // f.assertExhaustive(animal); - } - }); - - describe("partition", () => { - it("should split an array into true and false", () => { - const arr = ["T1", "F1", "T2", "F2"]; - expect( - f.partition(arr, (s: string) => s.startsWith("T")) - ).to.deep.equal([ - ["T1", "T2"], - ["F1", "F2"], - ]); - }); - - it("can handle an empty array", () => { - expect( - f.partition([], (s: string) => s.startsWith("T")) - ).to.deep.equal([[], []]); - }); - }); -}); diff --git a/src/test/functions/env.spec.ts b/src/test/functions/env.spec.ts deleted file mode 100644 index 8679bfee782..00000000000 --- a/src/test/functions/env.spec.ts +++ /dev/null @@ -1,388 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; -import { sync as rimraf } from "rimraf"; -import { expect } from "chai"; - -import * as env from "../../functions/env"; -import { previews } from "../../previews"; - -describe("functions/env", () => { - describe("parse", () => { - const tests: { description: string; input: string; want: Record }[] = [ - { - description: "should parse values with trailing spaces", - input: "FOO=foo ", - want: { FOO: "foo" }, - }, - { - description: "should parse values with trailing spaces (single quotes)", - input: "FOO='foo' ", - want: { FOO: "foo" }, - }, - { - description: "should parse values with trailing spaces (double quotes)", - input: 'FOO="foo" ', - want: { FOO: "foo" }, - }, - { - description: "should parse double quoted, multi-line values", - input: ` -FOO="foo1 -foo2" -BAR=bar -`, - want: { FOO: "foo1\nfoo2", BAR: "bar" }, - }, - { - description: "should parse many double quoted values", - input: 'FOO="foo"\nBAR="bar"', - want: { FOO: "foo", BAR: "bar" }, - }, - { - description: "should parse many single quoted values", - input: "FOO='foo'\nBAR='bar'", - want: { FOO: "foo", BAR: "bar" }, - }, - { - description: "should parse mix of double and single quoted values", - input: `FOO="foo"\nBAR='bar'`, - want: { FOO: "foo", BAR: "bar" }, - }, - { - description: "should parse double quoted with escaped newlines", - input: 'FOO="foo1\\nfoo2"\nBAR=bar', - want: { FOO: "foo1\nfoo2", BAR: "bar" }, - }, - { - description: "should leave single quotes when double quoted", - input: `FOO="'foo'"`, - want: { FOO: "'foo'" }, - }, - { - description: "should leave double quotes when single quoted", - input: `FOO='"foo"'`, - want: { FOO: '"foo"' }, - }, - { - description: "should unescape escape characters for double quoted values", - input: 'FOO="foo1\\"foo2"', - want: { FOO: 'foo1"foo2' }, - }, - { - description: "should leave escape characters intact for single quoted values", - input: "FOO='foo1\\'foo2'", - want: { FOO: "foo1\\'foo2" }, - }, - { - description: "should leave escape characters intact for unquoted values", - input: "FOO=foo1\\'foo2", - want: { FOO: "foo1\\'foo2" }, - }, - { - description: "should parse empty value", - input: "FOO=", - want: { FOO: "" }, - }, - { - description: "should parse keys with leading spaces", - input: " FOO=foo ", - want: { FOO: "foo" }, - }, - { - description: "should parse values with trailing spaces (unquoted)", - input: "FOO=foo ", - want: { FOO: "foo" }, - }, - { - description: "should parse values with trailing spaces (single quoted)", - input: "FOO='foo ' ", - want: { FOO: "foo " }, - }, - { - description: "should parse values with trailing spaces (double quoted)", - input: 'FOO="foo " ', - want: { FOO: "foo " }, - }, - { - description: "should throw away unquoted values following #", - input: "FOO=foo#bar", - want: { FOO: "foo" }, - }, - { - description: "should keep values following # in singqle quotes", - input: "FOO='foo#bar'", - want: { FOO: "foo#bar" }, - }, - { - description: "should keep values following # in double quotes", - input: 'FOO="foo#bar"', - want: { FOO: "foo#bar" }, - }, - { - description: "should ignore leading/trailing spaces before the separator (unquoted)", - input: "FOO = foo", - want: { FOO: "foo" }, - }, - { - description: "should ignore leading/trailing spaces before the separator (single quotes)", - input: "FOO = 'foo'", - want: { FOO: "foo" }, - }, - { - description: "should ignore leading/trailing spaces before the separator (double quotes)", - input: 'FOO = "foo"', - want: { FOO: "foo" }, - }, - { - description: "should handle empty values", - input: ` -FOO= -BAR= "blah" -`, - want: { FOO: "", BAR: "blah" }, - }, - { - description: "should handle quoted values after a newline", - input: ` -FOO= -"blah" -`, - want: { FOO: "blah" }, - }, - { - description: "should ignore comments", - input: ` - FOO=foo # comment - # line comment 1 - # line comment 2 - BAR=bar # another comment - `, - want: { FOO: "foo", BAR: "bar" }, - }, - { - description: "should ignore empty lines", - input: ` - FOO=foo - - BAR=bar - - `, - want: { FOO: "foo", BAR: "bar" }, - }, - ]; - - tests.forEach(({ description, input, want }) => { - it(description, () => { - const { envs, errors } = env.parse(input); - expect(envs).to.deep.equal(want); - expect(errors).to.be.empty; - }); - }); - - it("should catch invalid lines", () => { - expect( - env.parse(` -BAR### -FOO=foo -// not a comment -=missing key -`) - ).to.deep.equal({ - envs: { FOO: "foo" }, - errors: ["BAR###", "// not a comment", "=missing key"], - }); - }); - }); - - describe("validateKey", () => { - it("accepts valid keys", () => { - const keys = ["FOO", "ABC_EFG", "A1_B2"]; - keys.forEach((key) => { - expect(() => { - env.validateKey(key); - }).not.to.throw(); - }); - }); - - it("throws error given invalid keys", () => { - const keys = ["", "1F", "B=C"]; - keys.forEach((key) => { - expect(() => { - env.validateKey(key); - }).to.throw("must start with"); - }); - }); - - it("throws error given reserved keys", () => { - const keys = [ - "FIREBASE_CONFIG", - "FUNCTION_TARGET", - "FUNCTION_SIGNATURE_TYPE", - "K_SERVICE", - "K_REVISION", - "PORT", - "K_CONFIGURATION", - ]; - keys.forEach((key) => { - expect(() => { - env.validateKey(key); - }).to.throw("reserved for internal use"); - }); - }); - - it("throws error given keys with a reserved prefix", () => { - expect(() => { - env.validateKey("X_GOOGLE_FOOBAR"); - }).to.throw("starts with a reserved prefix"); - - expect(() => { - env.validateKey("FIREBASE_FOOBAR"); - }).to.throw("starts with a reserved prefix"); - }); - }); - - describe("loadUserEnvs", () => { - const createEnvFiles = (sourceDir: string, envs: Record): void => { - for (const [filename, data] of Object.entries(envs)) { - fs.writeFileSync(path.join(sourceDir, filename), data); - } - }; - const projectInfo = { projectId: "my-project", projectAlias: "dev" }; - let tmpdir: string; - - before(() => { - previews.dotenv = true; - }); - - after(() => { - previews.dotenv = false; - }); - - beforeEach(() => { - tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "test")); - }); - - afterEach(() => { - rimraf(tmpdir); - expect(() => { - fs.statSync(tmpdir); - }).to.throw; - }); - - it("loads nothing if .env files are missing", () => { - expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({}); - }); - - it("loads envs from .env file", () => { - createEnvFiles(tmpdir, { - ".env": "FOO=foo\nBAR=bar", - }); - - expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ - FOO: "foo", - BAR: "bar", - }); - }); - - it("loads envs from .env file, ignoring comments", () => { - createEnvFiles(tmpdir, { - ".env": "# THIS IS A COMMENT\nFOO=foo # inline comments\nBAR=bar", - }); - - expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ - FOO: "foo", - BAR: "bar", - }); - }); - - it("loads envs from .env. file", () => { - createEnvFiles(tmpdir, { - [`.env.${projectInfo.projectId}`]: "FOO=foo\nBAR=bar", - }); - - expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ - FOO: "foo", - BAR: "bar", - }); - }); - - it("loads envs from .env. file", () => { - createEnvFiles(tmpdir, { - [`.env.${projectInfo.projectAlias}`]: "FOO=foo\nBAR=bar", - }); - - expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ - FOO: "foo", - BAR: "bar", - }); - }); - - it("loads envs, preferring ones from .env.", () => { - createEnvFiles(tmpdir, { - ".env": "FOO=bad\nBAR=bar", - [`.env.${projectInfo.projectId}`]: "FOO=good", - }); - - expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ - FOO: "good", - BAR: "bar", - }); - }); - - it("loads envs, preferring ones from .env.", () => { - createEnvFiles(tmpdir, { - ".env": "FOO=bad\nBAR=bar", - [`.env.${projectInfo.projectAlias}`]: "FOO=good", - }); - - expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({ - FOO: "good", - BAR: "bar", - }); - }); - - it("throws an error if both .env. and .env. exists", () => { - createEnvFiles(tmpdir, { - ".env": "FOO=foo\nBAR=bar", - [`.env.${projectInfo.projectId}`]: "FOO=not-foo", - [`.env.${projectInfo.projectAlias}`]: "FOO=not-foo", - }); - - expect(() => { - env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); - }).to.throw("Can't have both"); - }); - - it("throws an error .env file is invalid", () => { - createEnvFiles(tmpdir, { - ".env": "BAH: foo", - }); - - expect(() => { - env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); - }).to.throw("Failed to load"); - }); - - it("throws an error .env file contains invalid keys", () => { - createEnvFiles(tmpdir, { - ".env": "FOO=foo", - [`.env.${projectInfo.projectId}`]: "Foo=bad-key", - }); - - expect(() => { - env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); - }).to.throw("Failed to load"); - }); - - it("throws an error .env file contains reserved keys", () => { - createEnvFiles(tmpdir, { - ".env": "FOO=foo\nPORT=100", - }); - - expect(() => { - env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir }); - }).to.throw("Failed to load"); - }); - }); -}); diff --git a/src/test/functions/functionsLog.spec.ts b/src/test/functions/functionsLog.spec.ts deleted file mode 100644 index 44ba38e593f..00000000000 --- a/src/test/functions/functionsLog.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as functionsLog from "../../functions/functionslog"; -import { logger } from "../../logger"; -import { previews } from "../../previews"; - -describe("functionsLog", () => { - describe("getApiFilter", () => { - it("should return base api filter for v1 functions", () => { - previews.functionsv2 = false; - expect(functionsLog.getApiFilter(undefined)).to.eq('resource.type="cloud_function"'); - }); - - it("should return base api filter for v1&v2 functions", () => { - previews.functionsv2 = true; - expect(functionsLog.getApiFilter(undefined)).to.eq( - 'resource.type="cloud_function" OR ' + - '(resource.type="cloud_run_revision" AND ' + - 'labels."goog-managed-by"="cloudfunctions")' - ); - }); - - it("should return list api filter for v1 functions", () => { - previews.functionsv2 = false; - expect(functionsLog.getApiFilter("fn1,fn2")).to.eq( - 'resource.type="cloud_function"\n' + - '(resource.labels.function_name="fn1" OR ' + - 'resource.labels.function_name="fn2")' - ); - }); - - it("should return list api filter for v1&v2 functions", () => { - previews.functionsv2 = true; - expect(functionsLog.getApiFilter("fn1,fn2")).to.eq( - 'resource.type="cloud_function" OR ' + - '(resource.type="cloud_run_revision" AND ' + - 'labels."goog-managed-by"="cloudfunctions")\n' + - '(resource.labels.function_name="fn1" OR ' + - 'resource.labels.service_name="fn1" OR ' + - 'resource.labels.function_name="fn2" OR ' + - 'resource.labels.service_name="fn2")' - ); - }); - }); - - describe("logEntries", () => { - let sandbox: sinon.SinonSandbox; - let loggerStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - loggerStub = sandbox.stub(logger, "info"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should log no entries", () => { - functionsLog.logEntries([]); - - expect(loggerStub).to.have.been.calledOnce; - expect(loggerStub).to.be.calledWith("No log entries found."); - }); - - it("should log entries", () => { - const entries = [ - { - logName: "log1", - resource: { - labels: { - function_name: "fn1", - }, - }, - receiveTimestamp: "0000000", - }, - { - logName: "log2", - resource: { - labels: { - service_name: "fn2", - }, - }, - receiveTimestamp: "0000000", - timestamp: "1111", - severity: "DEBUG", - textPayload: "payload", - }, - ]; - - functionsLog.logEntries(entries); - - expect(loggerStub).to.have.been.calledTwice; - expect(loggerStub.firstCall).to.be.calledWith("1111 D fn2: payload"); - expect(loggerStub.secondCall).to.be.calledWith("--- ? fn1: "); - }); - }); -}); diff --git a/src/test/gcp/cloudfunctions.spec.ts b/src/test/gcp/cloudfunctions.spec.ts deleted file mode 100644 index e491c52781d..00000000000 --- a/src/test/gcp/cloudfunctions.spec.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as api from "../../api"; - -import * as backend from "../../deploy/functions/backend"; -import * as cloudfunctions from "../../gcp/cloudfunctions"; - -describe("cloudfunctions", () => { - const FUNCTION_NAME: backend.TargetIds = { - id: "id", - region: "region", - project: "project", - }; - - // Omit a random trigger to make this compile - const ENDPOINT: Omit = { - platform: "gcfv1", - ...FUNCTION_NAME, - entryPoint: "function", - runtime: "nodejs16", - }; - - const CLOUD_FUNCTION: Omit = { - name: "projects/project/locations/region/functions/id", - entryPoint: "function", - runtime: "nodejs16", - }; - - const HAVE_CLOUD_FUNCTION: cloudfunctions.CloudFunction = { - ...CLOUD_FUNCTION, - buildId: "buildId", - versionId: 1, - updateTime: new Date(), - status: "ACTIVE", - }; - - describe("functionFromEndpoint", () => { - const UPLOAD_URL = "https://storage.googleapis.com/projects/-/buckets/sample/source.zip"; - it("should guard against version mixing", () => { - expect(() => { - cloudfunctions.functionFromEndpoint( - { ...ENDPOINT, platform: "gcfv2", httpsTrigger: {} }, - UPLOAD_URL - ); - }).to.throw; - }); - - it("should copy a minimal function", () => { - expect( - cloudfunctions.functionFromEndpoint({ ...ENDPOINT, httpsTrigger: {} }, UPLOAD_URL) - ).to.deep.equal({ - ...CLOUD_FUNCTION, - sourceUploadUrl: UPLOAD_URL, - httpsTrigger: {}, - }); - - const eventEndpoint = { - ...ENDPOINT, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - eventFilters: { - resource: "projects/p/topics/t", - }, - retry: false, - }, - }; - const eventGcfFunction = { - ...CLOUD_FUNCTION, - sourceUploadUrl: UPLOAD_URL, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "projects/p/topics/t", - failurePolicy: undefined, - }, - }; - expect(cloudfunctions.functionFromEndpoint(eventEndpoint, UPLOAD_URL)).to.deep.equal( - eventGcfFunction - ); - }); - - it("should copy trival fields", () => { - const fullEndpoint: backend.Endpoint = { - ...ENDPOINT, - httpsTrigger: {}, - availableMemoryMb: 128, - minInstances: 1, - maxInstances: 42, - vpcConnector: "connector", - vpcConnectorEgressSettings: "ALL_TRAFFIC", - ingressSettings: "ALLOW_ALL", - timeout: "15s", - serviceAccountEmail: "inlined@google.com", - labels: { - foo: "bar", - }, - environmentVariables: { - FOO: "bar", - }, - }; - - const fullGcfFunction: Omit = { - ...CLOUD_FUNCTION, - sourceUploadUrl: UPLOAD_URL, - httpsTrigger: {}, - labels: { - foo: "bar", - }, - environmentVariables: { - FOO: "bar", - }, - maxInstances: 42, - minInstances: 1, - vpcConnector: "connector", - vpcConnectorEgressSettings: "ALL_TRAFFIC", - ingressSettings: "ALLOW_ALL", - availableMemoryMb: 128, - timeout: "15s", - serviceAccountEmail: "inlined@google.com", - }; - - expect(cloudfunctions.functionFromEndpoint(fullEndpoint, UPLOAD_URL)).to.deep.equal( - fullGcfFunction - ); - }); - - it("should calculate non-trivial fields", () => { - const complexEndpoint: backend.Endpoint = { - ...ENDPOINT, - scheduleTrigger: {}, - }; - - const complexGcfFunction: Omit< - cloudfunctions.CloudFunction, - cloudfunctions.OutputOnlyFields - > = { - ...CLOUD_FUNCTION, - sourceUploadUrl: UPLOAD_URL, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: `projects/project/topics/${backend.scheduleIdForFunction(FUNCTION_NAME)}`, - }, - labels: { - "deployment-scheduled": "true", - }, - }; - - expect(cloudfunctions.functionFromEndpoint(complexEndpoint, UPLOAD_URL)).to.deep.equal( - complexGcfFunction - ); - }); - - it("detects task queue functions", () => { - const taskEndpoint: backend.Endpoint = { - ...ENDPOINT, - taskQueueTrigger: {}, - }; - const taskQueueFunction: Omit< - cloudfunctions.CloudFunction, - cloudfunctions.OutputOnlyFields - > = { - ...CLOUD_FUNCTION, - sourceUploadUrl: UPLOAD_URL, - httpsTrigger: {}, - labels: { - "deployment-taskqueue": "true", - }, - }; - - expect(cloudfunctions.functionFromEndpoint(taskEndpoint, UPLOAD_URL)).to.deep.equal( - taskQueueFunction - ); - }); - }); - - describe("endpointFromFunction", () => { - it("should copy a minimal version", () => { - expect( - cloudfunctions.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION, - httpsTrigger: {}, - }) - ).to.deep.equal({ ...ENDPOINT, httpsTrigger: {} }); - }); - - it("should translate event triggers", () => { - expect( - cloudfunctions.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "projects/p/topics/t", - failurePolicy: { - retry: {}, - }, - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - eventFilters: { - resource: "projects/p/topics/t", - }, - retry: true, - }, - }); - - // And again w/o the failure policy - expect( - cloudfunctions.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "projects/p/topics/t", - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - eventFilters: { - resource: "projects/p/topics/t", - }, - retry: false, - }, - }); - }); - - it("should transalte scheduled triggers", () => { - expect( - cloudfunctions.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION, - eventTrigger: { - eventType: "google.pubsub.topic.publish", - resource: "projects/p/topics/t", - failurePolicy: { - retry: {}, - }, - }, - labels: { - "deployment-scheduled": "true", - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - scheduleTrigger: {}, - labels: { - "deployment-scheduled": "true", - }, - }); - }); - - it("should translate task queue triggers", () => { - expect( - cloudfunctions.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION, - httpsTrigger: {}, - labels: { - "deployment-taskqueue": "true", - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - taskQueueTrigger: {}, - labels: { - "deployment-taskqueue": "true", - }, - }); - }); - - it("should copy optional fields", () => { - const extraFields: Partial = { - availableMemoryMb: 128, - minInstances: 1, - maxInstances: 42, - vpcConnector: "connector", - vpcConnectorEgressSettings: "ALL_TRAFFIC", - ingressSettings: "ALLOW_ALL", - serviceAccountEmail: "inlined@google.com", - timeout: "15s", - labels: { - foo: "bar", - }, - environmentVariables: { - FOO: "bar", - }, - }; - expect( - cloudfunctions.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION, - ...extraFields, - httpsTrigger: {}, - } as cloudfunctions.CloudFunction) - ).to.deep.equal({ - ...ENDPOINT, - ...extraFields, - httpsTrigger: {}, - }); - }); - }); - - describe("setInvokerCreate", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should reject on emtpy invoker array", async () => { - await expect(cloudfunctions.setInvokerCreate("project", "function", [])).to.be.rejected; - }); - - it("should reject if the setting the IAM policy fails", async () => { - apiRequestStub.onFirstCall().throws("Error calling set api."); - - await expect( - cloudfunctions.setInvokerCreate("project", "function", ["public"]) - ).to.be.rejectedWith("Failed to set the IAM Policy on the function function"); - expect(apiRequestStub).to.be.calledOnce; - }); - - it("should set a private policy on a function", async () => { - apiRequestStub.onFirstCall().callsFake((method: any, resource: any, options: any) => { - expect(options.data.policy).to.deep.eq({ - bindings: [ - { - role: "roles/cloudfunctions.invoker", - members: [], - }, - ], - etag: "", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(cloudfunctions.setInvokerCreate("project", "function", ["private"])).to.not.be - .rejected; - expect(apiRequestStub).to.be.calledOnce; - }); - - it("should set a public policy on a function", async () => { - apiRequestStub.onFirstCall().callsFake((method: any, resource: any, options: any) => { - expect(options.data.policy).to.deep.eq({ - bindings: [ - { - role: "roles/cloudfunctions.invoker", - members: ["allUsers"], - }, - ], - etag: "", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(cloudfunctions.setInvokerCreate("project", "function", ["public"])).to.not.be - .rejected; - expect(apiRequestStub).to.be.calledOnce; - }); - - it("should set the policy with a set of invokers with active policies", async () => { - apiRequestStub.onFirstCall().callsFake((method: any, resource: any, options: any) => { - options.data.policy.bindings[0].members.sort(); - expect(options.data.policy.bindings[0].members).to.deep.eq([ - "serviceAccount:service-account1@project.iam.gserviceaccount.com", - "serviceAccount:service-account2@project.iam.gserviceaccount.com", - "serviceAccount:service-account3@project.iam.gserviceaccount.com", - ]); - - return Promise.resolve(); - }); - - await expect( - cloudfunctions.setInvokerCreate("project", "function", [ - "service-account1@", - "service-account2@project.iam.gserviceaccount.com", - "service-account3@", - ]) - ).to.not.be.rejected; - expect(apiRequestStub).to.be.calledOnce; - }); - }); - - describe("setInvokerUpdate", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should reject on emtpy invoker array", async () => { - await expect(cloudfunctions.setInvokerUpdate("project", "function", [])).to.be.rejected; - }); - - it("should reject if the getting the IAM policy fails", async () => { - apiRequestStub.onFirstCall().throws("Error calling get api."); - - await expect( - cloudfunctions.setInvokerUpdate("project", "function", ["public"]) - ).to.be.rejectedWith("Failed to get the IAM Policy on the function function"); - - expect(apiRequestStub).to.be.called; - }); - - it("should reject if the setting the IAM policy fails", async () => { - apiRequestStub.onFirstCall().resolves({}); - apiRequestStub.onSecondCall().throws("Error calling set api."); - - await expect( - cloudfunctions.setInvokerUpdate("project", "function", ["public"]) - ).to.be.rejectedWith("Failed to set the IAM Policy on the function function"); - expect(apiRequestStub).to.be.calledTwice; - }); - - it("should set a basic policy on a function without any polices", async () => { - apiRequestStub.onFirstCall().resolves({}); - apiRequestStub.onSecondCall().callsFake((method: any, resource: any, options: any) => { - expect(options.data.policy).to.deep.eq({ - bindings: [ - { - role: "roles/cloudfunctions.invoker", - members: ["allUsers"], - }, - ], - etag: "", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(cloudfunctions.setInvokerUpdate("project", "function", ["public"])).to.not.be - .rejected; - expect(apiRequestStub).to.be.calledTwice; - }); - - it("should set the policy with private invoker with active policies", async () => { - apiRequestStub.onFirstCall().resolves({ - bindings: [ - { role: "random-role", members: ["user:pineapple"] }, - { role: "roles/cloudfunctions.invoker", members: ["some-service-account"] }, - ], - etag: "1234", - version: 3, - }); - apiRequestStub.onSecondCall().callsFake((method: any, resource: any, options: any) => { - expect(options.data.policy).to.deep.eq({ - bindings: [ - { role: "random-role", members: ["user:pineapple"] }, - { role: "roles/cloudfunctions.invoker", members: [] }, - ], - etag: "1234", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(cloudfunctions.setInvokerUpdate("project", "function", ["private"])).to.not.be - .rejected; - expect(apiRequestStub).to.be.calledTwice; - }); - - it("should set the policy with a set of invokers with active policies", async () => { - apiRequestStub.onFirstCall().resolves({}); - apiRequestStub.onSecondCall().callsFake((method: any, resource: any, options: any) => { - options.data.policy.bindings[0].members.sort(); - expect(options.data.policy.bindings[0].members).to.deep.eq([ - "serviceAccount:service-account1@project.iam.gserviceaccount.com", - "serviceAccount:service-account2@project.iam.gserviceaccount.com", - "serviceAccount:service-account3@project.iam.gserviceaccount.com", - ]); - - return Promise.resolve(); - }); - - await expect( - cloudfunctions.setInvokerUpdate("project", "function", [ - "service-account1@", - "service-account2@project.iam.gserviceaccount.com", - "service-account3@", - ]) - ).to.not.be.rejected; - expect(apiRequestStub).to.be.calledTwice; - }); - - it("should not set the policy if the set of invokers is the same as the current invokers", async () => { - apiRequestStub.onFirstCall().resolves({ - bindings: [ - { - role: "roles/cloudfunctions.invoker", - members: [ - "serviceAccount:service-account1@project.iam.gserviceaccount.com", - "serviceAccount:service-account3@project.iam.gserviceaccount.com", - "serviceAccount:service-account2@project.iam.gserviceaccount.com", - ], - }, - ], - etag: "1234", - version: 3, - }); - - await expect( - cloudfunctions.setInvokerUpdate("project", "function", [ - "service-account2@project.iam.gserviceaccount.com", - "service-account3@", - "service-account1@", - ]) - ).to.not.be.rejected; - expect(apiRequestStub).to.be.calledOnce; - }); - }); -}); diff --git a/src/test/gcp/cloudfunctionsv2.spec.ts b/src/test/gcp/cloudfunctionsv2.spec.ts deleted file mode 100644 index 7fbffa2ee5c..00000000000 --- a/src/test/gcp/cloudfunctionsv2.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { expect } from "chai"; - -import * as cloudfunctionsv2 from "../../gcp/cloudfunctionsv2"; -import * as backend from "../../deploy/functions/backend"; - -describe("cloudfunctionsv2", () => { - const FUNCTION_NAME: backend.TargetIds = { - id: "id", - region: "region", - project: "project", - }; - - // Omit a random trigger to get this fragment to compile. - const ENDPOINT: Omit = { - platform: "gcfv2", - ...FUNCTION_NAME, - entryPoint: "function", - runtime: "nodejs16", - }; - - const CLOUD_FUNCTION_V2_SOURCE: cloudfunctionsv2.StorageSource = { - bucket: "sample", - object: "source.zip", - generation: 42, - }; - - const CLOUD_FUNCTION_V2: Omit< - cloudfunctionsv2.CloudFunction, - cloudfunctionsv2.OutputOnlyFields - > = { - name: "projects/project/locations/region/functions/id", - buildConfig: { - entryPoint: "function", - runtime: "nodejs16", - source: { - storageSource: CLOUD_FUNCTION_V2_SOURCE, - }, - environmentVariables: {}, - }, - serviceConfig: {}, - }; - - const RUN_URI = "https://id-nonce-region-project.run.app"; - const HAVE_CLOUD_FUNCTION_V2: cloudfunctionsv2.CloudFunction = { - ...CLOUD_FUNCTION_V2, - serviceConfig: { - uri: RUN_URI, - }, - state: "ACTIVE", - updateTime: new Date(), - }; - - describe("megabytes", () => { - it("Should handle decimal SI units", () => { - expect(cloudfunctionsv2.megabytes("1000k")).to.equal(1); - expect(cloudfunctionsv2.megabytes("1.5M")).to.equal(1.5); - expect(cloudfunctionsv2.megabytes("1G")).to.equal(1000); - }); - it("Should handle binary SI units", () => { - expect(cloudfunctionsv2.megabytes("1Mi")).to.equal((1 << 20) / 1e6); - expect(cloudfunctionsv2.megabytes("1Gi")).to.equal((1 << 30) / 1e6); - }); - it("Should handle no unit", () => { - expect(cloudfunctionsv2.megabytes("100000")).to.equal(0.1); - expect(cloudfunctionsv2.megabytes("1e9")).to.equal(1000); - expect(cloudfunctionsv2.megabytes("1.5E6")).to.equal(1.5); - }); - }); - describe("functionFromEndpoint", () => { - const UPLOAD_URL = "https://storage.googleapis.com/projects/-/buckets/sample/source.zip"; - it("should guard against version mixing", () => { - expect(() => { - cloudfunctionsv2.functionFromEndpoint( - { ...ENDPOINT, httpsTrigger: {}, platform: "gcfv1" }, - CLOUD_FUNCTION_V2_SOURCE - ); - }).to.throw; - }); - - it("should copy a minimal function", () => { - expect( - cloudfunctionsv2.functionFromEndpoint( - { - ...ENDPOINT, - platform: "gcfv2", - httpsTrigger: {}, - }, - CLOUD_FUNCTION_V2_SOURCE - ) - ).to.deep.equal(CLOUD_FUNCTION_V2); - - const eventEndpoint: backend.Endpoint = { - ...ENDPOINT, - platform: "gcfv2", - eventTrigger: { - eventType: "google.cloud.audit.log.v1.written", - eventFilters: { - resource: "projects/p/regions/r/instances/i", - serviceName: "compute.googleapis.com", - }, - retry: false, - }, - }; - const eventGcfFunction: Omit< - cloudfunctionsv2.CloudFunction, - cloudfunctionsv2.OutputOnlyFields - > = { - ...CLOUD_FUNCTION_V2, - eventTrigger: { - eventType: "google.cloud.audit.log.v1.written", - eventFilters: [ - { - attribute: "resource", - value: "projects/p/regions/r/instances/i", - }, - { - attribute: "serviceName", - value: "compute.googleapis.com", - }, - ], - }, - }; - expect( - cloudfunctionsv2.functionFromEndpoint(eventEndpoint, CLOUD_FUNCTION_V2_SOURCE) - ).to.deep.equal(eventGcfFunction); - - expect( - cloudfunctionsv2.functionFromEndpoint( - { - ...ENDPOINT, - platform: "gcfv2", - taskQueueTrigger: {}, - }, - CLOUD_FUNCTION_V2_SOURCE - ) - ).to.deep.equal({ - ...CLOUD_FUNCTION_V2, - labels: { - "deployment-taskqueue": "true", - }, - }); - }); - - it("should copy trival fields", () => { - const fullEndpoint: backend.Endpoint = { - ...ENDPOINT, - httpsTrigger: {}, - platform: "gcfv2", - vpcConnector: "connector", - vpcConnectorEgressSettings: "ALL_TRAFFIC", - ingressSettings: "ALLOW_ALL", - serviceAccountEmail: "inlined@google.com", - labels: { - foo: "bar", - }, - environmentVariables: { - FOO: "bar", - }, - }; - - const fullGcfFunction: Omit< - cloudfunctionsv2.CloudFunction, - cloudfunctionsv2.OutputOnlyFields - > = { - ...CLOUD_FUNCTION_V2, - labels: { - foo: "bar", - }, - serviceConfig: { - ...CLOUD_FUNCTION_V2.serviceConfig, - environmentVariables: { - FOO: "bar", - }, - vpcConnector: "connector", - vpcConnectorEgressSettings: "ALL_TRAFFIC", - ingressSettings: "ALLOW_ALL", - serviceAccountEmail: "inlined@google.com", - }, - }; - - expect( - cloudfunctionsv2.functionFromEndpoint(fullEndpoint, CLOUD_FUNCTION_V2_SOURCE) - ).to.deep.equal(fullGcfFunction); - }); - - it("should calculate non-trivial fields", () => { - const complexEndpoint: backend.Endpoint = { - ...ENDPOINT, - platform: "gcfv2", - eventTrigger: { - eventType: cloudfunctionsv2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "projects/p/topics/t", - }, - retry: false, - }, - maxInstances: 42, - minInstances: 1, - timeout: "15s", - availableMemoryMb: 128, - }; - - const complexGcfFunction: Omit< - cloudfunctionsv2.CloudFunction, - cloudfunctionsv2.OutputOnlyFields - > = { - ...CLOUD_FUNCTION_V2, - eventTrigger: { - eventType: cloudfunctionsv2.PUBSUB_PUBLISH_EVENT, - pubsubTopic: "projects/p/topics/t", - }, - serviceConfig: { - ...CLOUD_FUNCTION_V2.serviceConfig, - maxInstanceCount: 42, - minInstanceCount: 1, - timeoutSeconds: 15, - availableMemory: "128M", - }, - }; - - expect( - cloudfunctionsv2.functionFromEndpoint(complexEndpoint, CLOUD_FUNCTION_V2_SOURCE) - ).to.deep.equal(complexGcfFunction); - }); - }); - - describe("endpointFromFunction", () => { - it("should copy a minimal version", () => { - expect(cloudfunctionsv2.endpointFromFunction(HAVE_CLOUD_FUNCTION_V2)).to.deep.equal({ - ...ENDPOINT, - httpsTrigger: {}, - platform: "gcfv2", - uri: RUN_URI, - }); - }); - - it("should translate event triggers", () => { - expect( - cloudfunctionsv2.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION_V2, - eventTrigger: { - eventType: cloudfunctionsv2.PUBSUB_PUBLISH_EVENT, - pubsubTopic: "projects/p/topics/t", - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - platform: "gcfv2", - uri: RUN_URI, - eventTrigger: { - eventType: cloudfunctionsv2.PUBSUB_PUBLISH_EVENT, - eventFilters: { - resource: "projects/p/topics/t", - }, - retry: false, - }, - }); - - // And again w/ a normal event trigger - expect( - cloudfunctionsv2.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION_V2, - eventTrigger: { - eventType: "google.cloud.audit.log.v1.written", - eventFilters: [ - { - attribute: "resource", - value: "projects/p/regions/r/instances/i", - }, - { - attribute: "serviceName", - value: "compute.googleapis.com", - }, - ], - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - platform: "gcfv2", - uri: RUN_URI, - eventTrigger: { - eventType: "google.cloud.audit.log.v1.written", - eventFilters: { - resource: "projects/p/regions/r/instances/i", - serviceName: "compute.googleapis.com", - }, - retry: false, - }, - }); - }); - - it("should translate task queue functions", () => { - expect( - cloudfunctionsv2.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION_V2, - labels: { "deployment-taskqueue": "true" }, - }) - ).to.deep.equal({ - ...ENDPOINT, - taskQueueTrigger: {}, - platform: "gcfv2", - uri: RUN_URI, - labels: { "deployment-taskqueue": "true" }, - }); - }); - - it("should copy optional fields", () => { - const extraFields: backend.ServiceConfiguration = { - vpcConnector: "connector", - vpcConnectorEgressSettings: "ALL_TRAFFIC", - ingressSettings: "ALLOW_ALL", - serviceAccountEmail: "inlined@google.com", - environmentVariables: { - FOO: "bar", - }, - }; - expect( - cloudfunctionsv2.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION_V2, - serviceConfig: { - ...HAVE_CLOUD_FUNCTION_V2.serviceConfig, - ...extraFields, - availableMemory: "128M", - }, - labels: { - foo: "bar", - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - platform: "gcfv2", - httpsTrigger: {}, - uri: RUN_URI, - ...extraFields, - availableMemoryMb: 128, - labels: { - foo: "bar", - }, - }); - }); - - it("should transform fields", () => { - const extraFields: backend.ServiceConfiguration = { - minInstances: 1, - maxInstances: 42, - timeout: "15s", - }; - - const extraGcfFields: Partial = { - minInstanceCount: 1, - maxInstanceCount: 42, - timeoutSeconds: 15, - }; - - expect( - cloudfunctionsv2.endpointFromFunction({ - ...HAVE_CLOUD_FUNCTION_V2, - serviceConfig: { - ...HAVE_CLOUD_FUNCTION_V2.serviceConfig, - ...extraGcfFields, - }, - }) - ).to.deep.equal({ - ...ENDPOINT, - platform: "gcfv2", - uri: RUN_URI, - httpsTrigger: {}, - ...extraFields, - }); - }); - }); -}); diff --git a/src/test/gcp/cloudscheduler.spec.ts b/src/test/gcp/cloudscheduler.spec.ts deleted file mode 100644 index a4626445b2a..00000000000 --- a/src/test/gcp/cloudscheduler.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { expect } from "chai"; -import * as _ from "lodash"; -import * as nock from "nock"; - -import { FirebaseError } from "../../error"; -import * as api from "../../api"; -import * as backend from "../../deploy/functions/backend"; -import * as cloudscheduler from "../../gcp/cloudscheduler"; - -const VERSION = "v1beta1"; - -const TEST_JOB: cloudscheduler.Job = { - name: "projects/test-project/locations/us-east1/jobs/test", - schedule: "every 5 minutes", - timeZone: "America/Los_Angeles", - httpTarget: { - uri: "https://afakeone.come", - httpMethod: "POST", - }, - retryConfig: {}, -}; - -describe("cloudscheduler", () => { - describe("createOrUpdateJob", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should create a job if none exists", async () => { - nock(api.cloudschedulerOrigin) - .get(`/${VERSION}/${TEST_JOB.name}`) - .reply(404, { context: { response: { statusCode: 404 } } }); - nock(api.cloudschedulerOrigin) - .post(`/${VERSION}/projects/test-project/locations/us-east1/jobs`) - .reply(200, TEST_JOB); - - const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); - - expect(response.body).to.deep.equal(TEST_JOB); - expect(nock.isDone()).to.be.true; - }); - - it("should do nothing if a functionally identical job exists", async () => { - const otherJob = _.cloneDeep(TEST_JOB); - otherJob.name = "something-different"; - nock(api.cloudschedulerOrigin).get(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); - - const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); - - expect(response).to.be.undefined; - expect(nock.isDone()).to.be.true; - }); - - it("should update if a job exists with the same name and a different schedule", async () => { - const otherJob = _.cloneDeep(TEST_JOB); - otherJob.schedule = "every 6 minutes"; - nock(api.cloudschedulerOrigin).get(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); - nock(api.cloudschedulerOrigin).patch(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); - - const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); - - expect(response.body).to.deep.equal(otherJob); - expect(nock.isDone()).to.be.true; - }); - - it("should update if a job exists with the same name but a different timeZone", async () => { - const otherJob = _.cloneDeep(TEST_JOB); - otherJob.timeZone = "America/New_York"; - nock(api.cloudschedulerOrigin).get(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); - nock(api.cloudschedulerOrigin).patch(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); - - const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); - - expect(response.body).to.deep.equal(otherJob); - expect(nock.isDone()).to.be.true; - }); - - it("should update if a job exists with the same name but a different retry config", async () => { - const otherJob = _.cloneDeep(TEST_JOB); - otherJob.retryConfig = { maxDoublings: 10 }; - nock(api.cloudschedulerOrigin).get(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); - nock(api.cloudschedulerOrigin).patch(`/${VERSION}/${TEST_JOB.name}`).reply(200, otherJob); - - const response = await cloudscheduler.createOrReplaceJob(TEST_JOB); - - expect(response.body).to.deep.equal(otherJob); - expect(nock.isDone()).to.be.true; - }); - - it("should error and exit if cloud resource location is not set", async () => { - nock(api.cloudschedulerOrigin) - .get(`/${VERSION}/${TEST_JOB.name}`) - .reply(404, { context: { response: { statusCode: 404 } } }); - nock(api.cloudschedulerOrigin) - .post(`/${VERSION}/projects/test-project/locations/us-east1/jobs`) - .reply(404, { context: { response: { statusCode: 404 } } }); - - await expect(cloudscheduler.createOrReplaceJob(TEST_JOB)).to.be.rejectedWith( - FirebaseError, - "Cloud resource location is not set" - ); - - expect(nock.isDone()).to.be.true; - }); - - it("should error and exit if cloud scheduler create request fail", async () => { - nock(api.cloudschedulerOrigin) - .get(`/${VERSION}/${TEST_JOB.name}`) - .reply(404, { context: { response: { statusCode: 404 } } }); - nock(api.cloudschedulerOrigin) - .post(`/${VERSION}/projects/test-project/locations/us-east1/jobs`) - .reply(400, { context: { response: { statusCode: 400 } } }); - - await expect(cloudscheduler.createOrReplaceJob(TEST_JOB)).to.be.rejectedWith( - FirebaseError, - "Failed to create scheduler job projects/test-project/locations/us-east1/jobs/test: HTTP Error: 400, Unknown Error" - ); - - expect(nock.isDone()).to.be.true; - }); - }); - - describe("jobFromEndpoint", () => { - const ENDPOINT: backend.Endpoint = { - platform: "gcfv1", - id: "id", - region: "region", - project: "project", - entryPoint: "id", - runtime: "nodejs16", - scheduleTrigger: { - schedule: "every 1 minutes", - }, - }; - it("should copy minimal fields", () => { - expect(cloudscheduler.jobFromEndpoint(ENDPOINT, "appEngineLocation")).to.deep.equal({ - name: "projects/project/locations/appEngineLocation/jobs/firebase-schedule-id-region", - schedule: "every 1 minutes", - pubsubTarget: { - topicName: "projects/project/topics/firebase-schedule-id-region", - attributes: { - scheduled: "true", - }, - }, - }); - }); - - it("should copy optional fields", () => { - expect( - cloudscheduler.jobFromEndpoint( - { - ...ENDPOINT, - scheduleTrigger: { - schedule: "every 1 minutes", - timeZone: "America/Los_Angeles", - retryConfig: { - maxDoublings: 2, - maxBackoffDuration: "20s", - minBackoffDuration: "1s", - maxRetryDuration: "60s", - }, - }, - }, - "appEngineLocation" - ) - ).to.deep.equal({ - name: "projects/project/locations/appEngineLocation/jobs/firebase-schedule-id-region", - schedule: "every 1 minutes", - timeZone: "America/Los_Angeles", - retryConfig: { - maxDoublings: 2, - maxBackoffDuration: "20s", - minBackoffDuration: "1s", - maxRetryDuration: "60s", - }, - pubsubTarget: { - topicName: "projects/project/topics/firebase-schedule-id-region", - attributes: { - scheduled: "true", - }, - }, - }); - }); - }); -}); diff --git a/src/test/gcp/run.spec.ts b/src/test/gcp/run.spec.ts deleted file mode 100644 index 95f12e59958..00000000000 --- a/src/test/gcp/run.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as run from "../../gcp/run"; -import { Client } from "../../apiv2"; - -describe("run", () => { - describe("setInvokerCreate", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - let client: Client; - - beforeEach(() => { - client = new Client({ - urlPrefix: "origin", - auth: true, - apiVersion: "v1", - }); - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(client, "post").throws("Unexpected API post call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should reject on emtpy invoker array", async () => { - await expect(run.setInvokerCreate("project", "service", [], client)).to.be.rejected; - }); - - it("should reject if the setting the IAM policy fails", async () => { - apiRequestStub.onFirstCall().throws("Error calling set api."); - - await expect( - run.setInvokerCreate("project", "service", ["public"], client) - ).to.be.rejectedWith("Failed to set the IAM Policy on the Service service"); - expect(apiRequestStub).to.be.calledOnce; - }); - - it("should set a private policy on a function", async () => { - apiRequestStub.onFirstCall().callsFake((path: string, json: any) => { - expect(json.policy).to.deep.eq({ - bindings: [ - { - role: "roles/run.invoker", - members: [], - }, - ], - etag: "", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(run.setInvokerCreate("project", "service", ["private"], client)).to.not.be - .rejected; - expect(apiRequestStub).to.be.calledOnce; - }); - - it("should set a public policy on a function", async () => { - apiRequestStub.onFirstCall().callsFake((path: string, json: any) => { - expect(json.policy).to.deep.eq({ - bindings: [ - { - role: "roles/run.invoker", - members: ["allUsers"], - }, - ], - etag: "", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(run.setInvokerCreate("project", "service", ["public"], client)).to.not.be - .rejected; - expect(apiRequestStub).to.be.calledOnce; - }); - - it("should set the policy with a set of invokers with active policies", async () => { - apiRequestStub.onFirstCall().callsFake((path: string, json: any) => { - json.policy.bindings[0].members.sort(); - expect(json.policy.bindings[0].members).to.deep.eq([ - "serviceAccount:service-account1@project.iam.gserviceaccount.com", - "serviceAccount:service-account2@project.iam.gserviceaccount.com", - "serviceAccount:service-account3@project.iam.gserviceaccount.com", - ]); - - return Promise.resolve(); - }); - - await expect( - run.setInvokerCreate( - "project", - "service", - [ - "service-account1@", - "service-account2@project.iam.gserviceaccount.com", - "service-account3@", - ], - client - ) - ).to.not.be.rejected; - expect(apiRequestStub).to.be.calledOnce; - }); - }); - - describe("setInvokerUpdate", () => { - describe("setInvokerCreate", () => { - let sandbox: sinon.SinonSandbox; - let apiPostStub: sinon.SinonStub; - let apiGetStub: sinon.SinonStub; - let client: Client; - - beforeEach(() => { - client = new Client({ - urlPrefix: "origin", - auth: true, - apiVersion: "v1", - }); - sandbox = sinon.createSandbox(); - apiPostStub = sandbox.stub(client, "post").throws("Unexpected API post call"); - apiGetStub = sandbox.stub(client, "get").throws("Unexpected API get call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should reject on emtpy invoker array", async () => { - await expect(run.setInvokerUpdate("project", "service", [])).to.be.rejected; - }); - - it("should reject if the getting the IAM policy fails", async () => { - apiGetStub.onFirstCall().throws("Error calling get api."); - - await expect( - run.setInvokerUpdate("project", "service", ["public"], client) - ).to.be.rejectedWith("Failed to get the IAM Policy on the Service service"); - - expect(apiGetStub).to.be.called; - }); - - it("should reject if the setting the IAM policy fails", async () => { - apiGetStub.resolves({ body: {} }); - apiPostStub.throws("Error calling set api."); - - await expect( - run.setInvokerUpdate("project", "service", ["public"], client) - ).to.be.rejectedWith("Failed to set the IAM Policy on the Service service"); - expect(apiGetStub).to.be.calledOnce; - expect(apiPostStub).to.be.calledOnce; - }); - - it("should set a basic policy on a function without any polices", async () => { - apiGetStub.onFirstCall().resolves({ body: {} }); - apiPostStub.onFirstCall().callsFake((path: string, json: any) => { - expect(json.policy).to.deep.eq({ - bindings: [ - { - role: "roles/run.invoker", - members: ["allUsers"], - }, - ], - etag: "", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(run.setInvokerUpdate("project", "service", ["public"], client)).to.not.be - .rejected; - expect(apiGetStub).to.be.calledOnce; - expect(apiPostStub).to.be.calledOnce; - }); - - it("should set the policy with private invoker with active policies", async () => { - apiGetStub.onFirstCall().resolves({ - body: { - bindings: [ - { role: "random-role", members: ["user:pineapple"] }, - { role: "roles/run.invoker", members: ["some-service-account"] }, - ], - etag: "1234", - version: 3, - }, - }); - apiPostStub.onFirstCall().callsFake((path: string, json: any) => { - expect(json.policy).to.deep.eq({ - bindings: [ - { role: "random-role", members: ["user:pineapple"] }, - { role: "roles/run.invoker", members: [] }, - ], - etag: "1234", - version: 3, - }); - - return Promise.resolve(); - }); - - await expect(run.setInvokerUpdate("project", "service", ["private"], client)).to.not.be - .rejected; - expect(apiGetStub).to.be.calledOnce; - expect(apiPostStub).to.be.calledOnce; - }); - - it("should set the policy with a set of invokers with active policies", async () => { - apiGetStub.onFirstCall().resolves({ body: {} }); - apiPostStub.onFirstCall().callsFake((path: string, json: any) => { - json.policy.bindings[0].members.sort(); - expect(json.policy.bindings[0].members).to.deep.eq([ - "serviceAccount:service-account1@project.iam.gserviceaccount.com", - "serviceAccount:service-account2@project.iam.gserviceaccount.com", - "serviceAccount:service-account3@project.iam.gserviceaccount.com", - ]); - - return Promise.resolve(); - }); - - await expect( - run.setInvokerUpdate( - "project", - "service", - [ - "service-account1@", - "service-account2@project.iam.gserviceaccount.com", - "service-account3@", - ], - client - ) - ).to.not.be.rejected; - expect(apiGetStub).to.be.calledOnce; - expect(apiPostStub).to.be.calledOnce; - }); - - it("should not set the policy if the set of invokers is the same as the current invokers", async () => { - apiGetStub.onFirstCall().resolves({ - body: { - bindings: [ - { - role: "roles/run.invoker", - members: [ - "serviceAccount:service-account1@project.iam.gserviceaccount.com", - "serviceAccount:service-account3@project.iam.gserviceaccount.com", - "serviceAccount:service-account2@project.iam.gserviceaccount.com", - ], - }, - ], - etag: "1234", - version: 3, - }, - }); - - await expect( - run.setInvokerUpdate( - "project", - "service", - [ - "service-account2@project.iam.gserviceaccount.com", - "service-account3@", - "service-account1@", - ], - client - ) - ).to.not.be.rejected; - expect(apiGetStub).to.be.calledOnce; - expect(apiPostStub).to.not.be.called; - }); - }); - }); -}); diff --git a/src/test/hosting/api.spec.ts b/src/test/hosting/api.spec.ts deleted file mode 100644 index f5b6c9590df..00000000000 --- a/src/test/hosting/api.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { expect } from "chai"; -import * as nock from "nock"; - -import * as api from "../../api"; -import * as hostingApi from "../../hosting/api"; - -const TEST_CHANNELS_RESPONSE = { - channels: [ - // domain exists in TEST_GET_DOMAINS_RESPONSE - { url: "https://my-site--ch1-4iyrl1uo.web.app" }, - // domain does not exist in TEST_GET_DOMAINS_RESPONSE - // we assume this domain was manually removed by - // the user from the identity api - { url: "https://my-site--ch2-ygd8582v.web.app" }, - ], -}; -const TEST_GET_DOMAINS_RESPONSE = { - authorizedDomains: [ - "my-site.firebaseapp.com", - "localhost", - "randomurl.com", - "my-site--ch1-4iyrl1uo.web.app", - // domain that should be removed - "my-site--expiredchannel-difhyc76.web.app", - ], -}; - -const EXPECTED_DOMAINS_RESPONSE = [ - "my-site.firebaseapp.com", - "localhost", - "randomurl.com", - "my-site--ch1-4iyrl1uo.web.app", -]; -const PROJECT_ID = "test-project"; -const SITE = "my-site"; - -describe("hosting", () => { - describe("getCleanDomains", () => { - afterEach(() => { - nock.cleanAll(); - }); - - it("should return the list of expected auth domains after syncing", async () => { - // mock listChannels response - nock(api.hostingApiOrigin) - .get(`/v1beta1/projects/${PROJECT_ID}/sites/${SITE}/channels`) - .query(() => true) - .reply(200, TEST_CHANNELS_RESPONSE); - // mock getAuthDomains response - nock(api.identityOrigin) - .get(`/admin/v2/projects/${PROJECT_ID}/config`) - .reply(200, TEST_GET_DOMAINS_RESPONSE); - - const res = await hostingApi.getCleanDomains(PROJECT_ID, SITE); - - expect(res).to.deep.equal(EXPECTED_DOMAINS_RESPONSE); - expect(nock.isDone()).to.be.true; - }); - }); -}); - -describe("normalizeName", () => { - const tests = [ - { in: "happy-path", out: "happy-path" }, - { in: "feature/branch", out: "feature-branch" }, - { in: "featuRe/Branch", out: "featuRe-Branch" }, - { in: "what/are:you_thinking", out: "what-are-you-thinking" }, - { in: "happyBranch", out: "happyBranch" }, - { in: "happy:branch", out: "happy-branch" }, - { in: "happy_branch", out: "happy-branch" }, - { in: "happy#branch", out: "happy-branch" }, - ]; - - for (const t of tests) { - it(`should handle the normalization of ${t.in}`, () => { - expect(hostingApi.normalizeName(t.in)).to.equal(t.out); - }); - } -}); diff --git a/src/test/hosting/expireUtils.spec.ts b/src/test/hosting/expireUtils.spec.ts deleted file mode 100644 index 168c2cd92c4..00000000000 --- a/src/test/hosting/expireUtils.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { expect } from "chai"; - -import { calculateChannelExpireTTL } from "../../hosting/expireUtils"; -import { FirebaseError } from "../../error"; - -describe("calculateChannelExpireTTL", () => { - const goodTests = [ - { input: "30d", want: 30 * 24 * 60 * 60 * 1000 }, - { input: "1d", want: 24 * 60 * 60 * 1000 }, - { input: "2d", want: 2 * 24 * 60 * 60 * 1000 }, - { input: "2h", want: 2 * 60 * 60 * 1000 }, - { input: "56m", want: 56 * 60 * 1000 }, - ]; - - for (const test of goodTests) { - it(`should be able to parse time ${test.input}`, () => { - const got = calculateChannelExpireTTL(test.input); - expect(got).to.equal(test.want, `unexpected output for ${test.input}`); - }); - } - - const badTests = [{ input: "1.5d" }, { input: "2x" }, { input: "2dd" }, { input: "0.5m" }]; - - for (const test of badTests) { - it(`should be able to parse time ${test.input}`, () => { - expect(() => calculateChannelExpireTTL(test.input)).to.throw( - FirebaseError, - /flag must be a duration string/ - ); - }); - } - - it("should throw if greater than 30d", () => { - expect(() => calculateChannelExpireTTL("31d")).to.throw( - FirebaseError, - /not be longer than 30d/ - ); - expect(() => calculateChannelExpireTTL(`${31 * 24}h`)).to.throw( - FirebaseError, - /not be longer than 30d/ - ); - }); -}); diff --git a/src/test/hosting/normalizedHostingConfigs.spec.ts b/src/test/hosting/normalizedHostingConfigs.spec.ts deleted file mode 100644 index 7b2df96789b..00000000000 --- a/src/test/hosting/normalizedHostingConfigs.spec.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { expect } from "chai"; -import { FirebaseError } from "../../error"; - -import { normalizedHostingConfigs } from "../../hosting/normalizedHostingConfigs"; - -describe("normalizedHostingConfigs", () => { - it("should fail if both site and target are specified", () => { - const singleHostingConfig = { site: "site", target: "target" }; - const cmdConfig = { - site: "default-site", - config: { get: () => singleHostingConfig }, - }; - expect(() => normalizedHostingConfigs(cmdConfig)).to.throw( - FirebaseError, - /configs should only include either/ - ); - - const hostingConfig = [{ site: "site", target: "target" }]; - const newCmdConfig = { - site: "default-site", - config: { get: () => hostingConfig }, - }; - expect(() => normalizedHostingConfigs(newCmdConfig)).to.throw( - FirebaseError, - /configs should only include either/ - ); - }); - - it("should not modify the config when resolving targets", () => { - const singleHostingConfig = { target: "target" }; - const cmdConfig = { - site: "default-site", - config: { get: () => singleHostingConfig }, - rc: { requireTarget: () => ["default-site"] }, - }; - normalizedHostingConfigs(cmdConfig, { resolveTargets: true }); - expect(singleHostingConfig).to.deep.equal({ target: "target" }); - }); - - describe("without an only parameter", () => { - const DEFAULT_SITE = "default-hosting-site"; - const baseConfig = { public: "public", ignore: ["firebase.json"] }; - const tests = [ - { - desc: "a normal hosting config", - cfg: Object.assign({}, baseConfig), - want: [Object.assign({}, baseConfig, { site: DEFAULT_SITE })], - }, - { - desc: "no hosting config", - want: [], - }, - { - desc: "a normal hosting config with a target", - cfg: Object.assign({}, baseConfig, { target: "main" }), - want: [Object.assign({}, baseConfig, { target: "main" })], - }, - { - desc: "a hosting config with multiple targets", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - want: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - }, - ]; - - for (const t of tests) { - it(`should be able to parse ${t.desc}`, () => { - const cmdConfig = { - site: DEFAULT_SITE, - config: { get: () => t.cfg }, - }; - const got = normalizedHostingConfigs(cmdConfig); - expect(got).to.deep.equal(t.want); - }); - } - }); - - describe("with an only parameter, resolving targets", () => { - const DEFAULT_SITE = "default-hosting-site"; - const TARGETED_SITE = "targeted-site"; - const baseConfig = { public: "public", ignore: ["firebase.json"] }; - const tests = [ - { - desc: "a normal hosting config, specifying the default site", - cfg: Object.assign({}, baseConfig), - only: `hosting:${DEFAULT_SITE}`, - want: [Object.assign({}, baseConfig, { site: DEFAULT_SITE })], - }, - { - desc: "a hosting config with multiple sites, no targets, specifying the second site", - cfg: [ - Object.assign({}, baseConfig, { site: DEFAULT_SITE }), - Object.assign({}, baseConfig, { site: "different-site" }), - ], - only: `hosting:different-site`, - want: [Object.assign({}, baseConfig, { site: "different-site" })], - }, - { - desc: "a normal hosting config with a target", - cfg: Object.assign({}, baseConfig, { target: "main" }), - only: "hosting:main", - want: [Object.assign({}, baseConfig, { target: "main", site: TARGETED_SITE })], - }, - { - desc: "a hosting config with multiple targets, specifying one", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - only: "hosting:t-two", - want: [Object.assign({}, baseConfig, { target: "t-two", site: TARGETED_SITE })], - }, - { - desc: "a hosting config with multiple targets, specifying all hosting", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - only: "hosting", - want: [ - Object.assign({}, baseConfig, { target: "t-one", site: TARGETED_SITE }), - Object.assign({}, baseConfig, { target: "t-two", site: TARGETED_SITE }), - ], - }, - { - desc: "a hosting config with multiple targets, specifying an invalid target", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - only: "hosting:t-three", - wantErr: /Hosting site or target.+t-three.+not detected/, - }, - { - desc: "a hosting config with multiple targets, with multiple matching targets", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-one" }), - ], - only: "hosting:t-one", - targetedSites: [TARGETED_SITE, TARGETED_SITE], - wantErr: /Hosting target.+t-one.+linked to multiple sites/, - }, - { - desc: "a hosting config with multiple sites but no targets, only all hosting", - cfg: [Object.assign({}, baseConfig), Object.assign({}, baseConfig)], - only: "hosting", - wantErr: /Must supply either "site" or "target"/, - }, - { - desc: "a hosting config with multiple sites but no targets, only an invalid target", - cfg: [Object.assign({}, baseConfig), Object.assign({}, baseConfig)], - only: "hosting:t-one", - wantErr: /Hosting site or target.+t-one.+not detected/, - }, - ]; - - for (const t of tests) { - it(`should be able to parse ${t.desc}`, () => { - if (!Array.isArray(t.targetedSites)) { - t.targetedSites = [TARGETED_SITE]; - } - const cmdConfig = { - site: DEFAULT_SITE, - only: t.only, - config: { get: () => t.cfg }, - rc: { requireTarget: () => t.targetedSites }, - }; - - if (t.wantErr) { - expect(() => normalizedHostingConfigs(cmdConfig, { resolveTargets: true })).to.throw( - FirebaseError, - t.wantErr - ); - } else { - const got = normalizedHostingConfigs(cmdConfig, { resolveTargets: true }); - expect(got).to.deep.equal(t.want); - } - }); - } - }); - - describe("with an except parameter, resolving targets", () => { - const DEFAULT_SITE = "default-hosting-site"; - const TARGETED_SITE = "targeted-site"; - const baseConfig = { public: "public", ignore: ["firebase.json"] }; - const tests = [ - { - desc: "a normal hosting config, omitting the default site", - cfg: Object.assign({}, baseConfig), - except: `hosting:${DEFAULT_SITE}`, - want: [], - }, - { - desc: "a hosting config with multiple sites, no targets, omitting the second site", - cfg: [ - Object.assign({}, baseConfig, { site: DEFAULT_SITE }), - Object.assign({}, baseConfig, { site: "different-site" }), - ], - except: `hosting:different-site`, - want: [Object.assign({}, baseConfig, { site: DEFAULT_SITE })], - }, - { - desc: "a normal hosting config with a target, omitting the target", - cfg: Object.assign({}, baseConfig, { target: "main" }), - except: "hosting:main", - want: [], - }, - { - desc: "a hosting config with multiple targets, omitting one", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - except: "hosting:t-two", - want: [Object.assign({}, baseConfig, { target: "t-one", site: TARGETED_SITE })], - }, - { - desc: "a hosting config with multiple targets, omitting all hosting", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - except: "hosting", - want: [], - }, - { - desc: "a hosting config with multiple targets, omitting an invalid target", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-two" }), - ], - except: "hosting:t-three", - want: [ - Object.assign({}, baseConfig, { target: "t-one", site: TARGETED_SITE }), - Object.assign({}, baseConfig, { target: "t-two", site: TARGETED_SITE }), - ], - }, - { - desc: "a hosting config with multiple targets, with multiple matching targets", - cfg: [ - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-one" }), - Object.assign({}, baseConfig, { target: "t-other" }), - ], - except: "hosting:t-other", - targetedSites: [TARGETED_SITE, TARGETED_SITE], - wantErr: /Hosting target.+t-one.+linked to multiple sites/, - }, - { - desc: "a hosting config with multiple sites but no targets, only all hosting", - cfg: [Object.assign({}, baseConfig), Object.assign({}, baseConfig)], - except: "hosting:site", - wantErr: /Must supply either "site" or "target"/, - }, - ]; - - for (const t of tests) { - it(`should be able to parse ${t.desc}`, () => { - if (!Array.isArray(t.targetedSites)) { - t.targetedSites = [TARGETED_SITE]; - } - const cmdConfig = { - site: DEFAULT_SITE, - except: t.except, - config: { get: () => t.cfg }, - rc: { requireTarget: () => t.targetedSites }, - }; - - if (t.wantErr) { - expect(() => normalizedHostingConfigs(cmdConfig, { resolveTargets: true })).to.throw( - FirebaseError, - t.wantErr - ); - } else { - const got = normalizedHostingConfigs(cmdConfig, { resolveTargets: true }); - expect(got).to.deep.equal(t.want); - } - }); - } - }); -}); diff --git a/src/test/init/features/firestore.spec.ts b/src/test/init/features/firestore.spec.ts deleted file mode 100644 index 012178165f3..00000000000 --- a/src/test/init/features/firestore.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { expect } from "chai"; -import * as _ from "lodash"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../../error"; -import * as firestore from "../../../init/features/firestore"; -import * as indexes from "../../../init/features/firestore/indexes"; -import * as rules from "../../../init/features/firestore/rules"; -import * as requirePermissions from "../../../requirePermissions"; -import * as apiEnabled from "../../../ensureApiEnabled"; -import * as checkDatabaseType from "../../../firestore/checkDatabaseType"; - -describe("firestore", () => { - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let checkApiStub: sinon.SinonStub; - let checkDbTypeStub: sinon.SinonStub; - - beforeEach(() => { - checkApiStub = sandbox.stub(apiEnabled, "check"); - checkDbTypeStub = sandbox.stub(checkDatabaseType, "checkDatabaseType"); - - // By default, mock Firestore enabled in Native mode - checkApiStub.returns(true); - checkDbTypeStub.returns("CLOUD_FIRESTORE"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("doSetup", () => { - it("should require access, set up rules and indices, ensure cloud resource location set", async () => { - const requirePermissionsStub = sandbox - .stub(requirePermissions, "requirePermissions") - .resolves(); - const initIndexesStub = sandbox.stub(indexes, "initIndexes").resolves(); - const initRulesStub = sandbox.stub(rules, "initRules").resolves(); - - const setup = { config: {}, projectId: "my-project-123", projectLocation: "us-central1" }; - - await firestore.doSetup(setup, {}, {}); - - expect(requirePermissionsStub).to.have.been.calledOnce; - expect(initRulesStub).to.have.been.calledOnce; - expect(initIndexesStub).to.have.been.calledOnce; - expect(_.get(setup, "config.firestore")).to.deep.equal({}); - }); - - it("should error when cloud resource location is not set", async () => { - const setup = { config: {}, projectId: "my-project-123" }; - - await expect(firestore.doSetup(setup, {}, {})).to.eventually.be.rejectedWith( - FirebaseError, - "Cloud resource location is not set" - ); - }); - - it("should error when the firestore API is not enabled", async () => { - checkApiStub.returns(false); - - const setup = { config: {}, projectId: "my-project-123" }; - - await expect(firestore.doSetup(setup, {}, {})).to.eventually.be.rejectedWith( - FirebaseError, - "It looks like you haven't used Cloud Firestore" - ); - }); - - it("should error when firestore is in the wrong mode", async () => { - checkApiStub.returns(true); - checkDbTypeStub.returns("CLOUD_DATASTORE_COMPATIBILITY"); - - const setup = { config: {}, projectId: "my-project-123" }; - - await expect(firestore.doSetup(setup, {}, {})).to.eventually.be.rejectedWith( - FirebaseError, - "It looks like this project is using Cloud Datastore or Cloud Firestore in Datastore mode." - ); - }); - }); -}); diff --git a/src/test/init/features/storage.spec.ts b/src/test/init/features/storage.spec.ts deleted file mode 100644 index 5c2ddd11442..00000000000 --- a/src/test/init/features/storage.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expect } from "chai"; -import * as _ from "lodash"; -import * as sinon from "sinon"; - -import { FirebaseError } from "../../../error"; -import { Config } from "../../../config"; -import { doSetup } from "../../../init/features/storage"; -import * as prompt from "../../../prompt"; - -describe("storage", () => { - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let askWriteProjectFileStub: sinon.SinonStub; - let promptStub: sinon.SinonStub; - - beforeEach(() => { - askWriteProjectFileStub = sandbox.stub(Config.prototype, "askWriteProjectFile"); - promptStub = sandbox.stub(prompt, "promptOnce"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("doSetup", () => { - it("should set up the correct properties in the project", async () => { - const setup = { - config: {}, - rcfile: {}, - projectId: "my-project-123", - projectLocation: "us-central", - }; - promptStub.returns("storage.rules"); - askWriteProjectFileStub.resolves(); - - await doSetup(setup, new Config("/path/to/src", {})); - - expect(_.get(setup, "config.storage.rules")).to.deep.equal("storage.rules"); - }); - - it("should error when cloud resource location is not set", async () => { - const setup = { - config: {}, - rcfile: {}, - projectId: "my-project-123", - }; - - await expect(doSetup(setup, new Config("/path/to/src", {}))).to.eventually.be.rejectedWith( - FirebaseError, - "Cloud resource location is not set" - ); - }); - }); -}); diff --git a/src/test/listFiles.spec.ts b/src/test/listFiles.spec.ts deleted file mode 100644 index af1fbd0c755..00000000000 --- a/src/test/listFiles.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expect } from "chai"; -import { resolve } from "path"; - -import { listFiles } from "../listFiles"; - -describe("listFiles", () => { - // for details, see the file structure and firebase.json in test/fixtures/ignores - it("should ignore firebase-debug.log, specified ignores, and nothing else", () => { - const fileNames = listFiles(resolve(__dirname, "./fixtures/ignores"), [ - "**/.*", - "firebase.json", - "ignored.txt", - "ignored/**/*.txt", - ]); - expect(fileNames).to.deep.equal(["index.html", "ignored/index.html", "present/index.html"]); - }); - - it("should allow us to not specify additional ignores", () => { - const fileNames = listFiles(resolve(__dirname, "./fixtures/ignores")); - expect(fileNames.sort()).to.have.members([ - ".hiddenfile", - "firebase.json", - "ignored.txt", - "ignored/deeper/index.txt", - "ignored/ignore.txt", - "ignored/index.html", - "index.html", - "present/index.html", - ]); - }); -}); diff --git a/src/test/localFunction.spec.js b/src/test/localFunction.spec.js deleted file mode 100644 index 2b47d7b2879..00000000000 --- a/src/test/localFunction.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -"use strict"; - -var chai = require("chai"); -var expect = chai.expect; - -var LocalFunction = require("../localFunction"); - -describe("localFunction._constructAuth", function () { - var lf = new LocalFunction({}); - - describe("#_constructAuth", function () { - var constructAuth = lf._constructAuth; - - it("warn if opts.auth and opts.authType are conflicting", function () { - expect(function () { - return constructAuth({ uid: "something" }, "UNAUTHENTICATED"); - }).to.throw("incompatible"); - - expect(function () { - return constructAuth({ uid: "something" }, "ADMIN"); - }).to.throw("incompatible"); - }); - - it("construct the correct auth for admin users", function () { - expect(constructAuth(undefined, "ADMIN")).to.deep.equal({ admin: true }); - }); - - it("construct the correct auth for unauthenticated users", function () { - expect(constructAuth(undefined, "UNAUTHENTICATED")).to.deep.equal({ - admin: false, - }); - }); - - it("construct the correct auth for authenticated users", function () { - expect(constructAuth(undefined, "USER")).to.deep.equal({ - variable: { uid: "", token: {} }, - }); - expect(constructAuth({ uid: "11" }, "USER")).to.deep.equal({ - variable: { uid: "11", token: {} }, - }); - }); - - it("leaves auth untouched if it already follows wire format", function () { - var auth = { variable: { uid: "something" } }; - expect(constructAuth(auth)).to.deep.equal(auth); - }); - }); - - describe("localFunction._makeFirestoreValue", function () { - var makeFirestoreValue = lf._makeFirestoreValue; - - it("returns {} when there is no data", function () { - expect(makeFirestoreValue()).to.deep.equal({}); - expect(makeFirestoreValue(null)).to.deep.equal({}); - expect(makeFirestoreValue({})).to.deep.equal({}); - }); - - it("throws error when data is not key-value pairs", function () { - expect(function () { - return makeFirestoreValue("string"); - }).to.throw(Error); - }); - }); -}); diff --git a/src/test/management/apps.spec.ts b/src/test/management/apps.spec.ts deleted file mode 100644 index 2ac60b26d84..00000000000 --- a/src/test/management/apps.spec.ts +++ /dev/null @@ -1,791 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as fs from "fs"; - -import * as api from "../../api"; -import { - AndroidAppMetadata, - AppPlatform, - createAndroidApp, - createIosApp, - createWebApp, - getAppConfig, - getAppConfigFile, - getAppPlatform, - IosAppMetadata, - listFirebaseApps, - WebAppMetadata, -} from "../../management/apps"; -import * as pollUtils from "../../operation-poller"; -import { FirebaseError } from "../../error"; - -const PROJECT_ID = "the-best-firebase-project"; -const OPERATION_RESOURCE_NAME_1 = "operations/cp.11111111111111111"; -const APP_ID = "appId"; -const IOS_APP_BUNDLE_ID = "bundleId"; -const IOS_APP_STORE_ID = "appStoreId"; -const IOS_APP_DISPLAY_NAME = "iOS app"; -const ANDROID_APP_PACKAGE_NAME = "com.google.packageName"; -const ANDROID_APP_DISPLAY_NAME = "Android app"; -const WEB_APP_DISPLAY_NAME = "Web app"; - -function generateIosAppList(counts: number): IosAppMetadata[] { - return Array.from(Array(counts), (_, i: number) => ({ - name: `projects/project-id-${i}/apps/app-id-${i}`, - projectId: `project-id`, - appId: `app-id-${i}`, - platform: AppPlatform.IOS, - displayName: `Project ${i}`, - bundleId: `bundle-id-${i}`, - })); -} - -function generateAndroidAppList(counts: number): AndroidAppMetadata[] { - return Array.from(Array(counts), (_, i: number) => ({ - name: `projects/project-id-${i}/apps/app-id-${i}`, - projectId: `project-id`, - appId: `app-id-${i}`, - platform: AppPlatform.ANDROID, - displayName: `Project ${i}`, - packageName: `package.name.app${i}`, - })); -} - -function generateWebAppList(counts: number): WebAppMetadata[] { - return Array.from(Array(counts), (_, i: number) => ({ - name: `projects/project-id-${i}/apps/app-id-${i}`, - projectId: `project-id`, - appId: `app-id-${i}`, - platform: AppPlatform.WEB, - displayName: `Project ${i}`, - })); -} - -describe("App management", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - let pollOperationStub: sinon.SinonStub; - let readFileSyncStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - pollOperationStub = sandbox.stub(pollUtils, "pollOperation").throws("Unexpected poll call"); - readFileSyncStub = sandbox.stub(fs, "readFileSync").throws("Unxpected readFileSync call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("getAppPlatform", () => { - it("should return the iOS platform", () => { - expect(getAppPlatform("IOS")).to.equal(AppPlatform.IOS); - expect(getAppPlatform("iOS")).to.equal(AppPlatform.IOS); - expect(getAppPlatform("Ios")).to.equal(AppPlatform.IOS); - }); - - it("should return the Android platform", () => { - expect(getAppPlatform("Android")).to.equal(AppPlatform.ANDROID); - expect(getAppPlatform("ANDROID")).to.equal(AppPlatform.ANDROID); - expect(getAppPlatform("aNDroiD")).to.equal(AppPlatform.ANDROID); - }); - - it("should return the Web platform", () => { - expect(getAppPlatform("Web")).to.equal(AppPlatform.WEB); - expect(getAppPlatform("WEB")).to.equal(AppPlatform.WEB); - expect(getAppPlatform("wEb")).to.equal(AppPlatform.WEB); - }); - - it("should return the ANY platform", () => { - expect(getAppPlatform("")).to.equal(AppPlatform.ANY); - }); - - it("should throw if the platform is unknown", () => { - expect(() => getAppPlatform("unknown")).to.throw( - FirebaseError, - "Unexpected platform. Only iOS, Android, and Web apps are supported" - ); - }); - }); - - describe("createIosApp", () => { - it("should resolve with app data if it succeeds", async () => { - const expectedAppMetadata = { - appId: APP_ID, - displayName: IOS_APP_DISPLAY_NAME, - bundleId: IOS_APP_BUNDLE_ID, - appStoreId: IOS_APP_STORE_ID, - }; - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_1 } }); - pollOperationStub.onFirstCall().resolves(expectedAppMetadata); - - const resultAppInfo = await createIosApp(PROJECT_ID, { - displayName: IOS_APP_DISPLAY_NAME, - bundleId: IOS_APP_BUNDLE_ID, - appStoreId: IOS_APP_STORE_ID, - }); - - expect(resultAppInfo).to.deep.equal(expectedAppMetadata); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/iosApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: IOS_APP_DISPLAY_NAME, - bundleId: IOS_APP_BUNDLE_ID, - appStoreId: IOS_APP_STORE_ID, - }, - } - ); - expect(pollOperationStub).to.be.calledOnceWith({ - pollerName: "Create iOS app Poller", - apiOrigin: api.firebaseApiOrigin, - apiVersion: "v1beta1", - operationResourceName: OPERATION_RESOURCE_NAME_1, - }); - }); - - it("should reject if app creation api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await createIosApp(PROJECT_ID, { - displayName: IOS_APP_DISPLAY_NAME, - bundleId: IOS_APP_BUNDLE_ID, - appStoreId: IOS_APP_STORE_ID, - }); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to create iOS app for project ${PROJECT_ID}. See firebase-debug.log for more info.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/iosApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: IOS_APP_DISPLAY_NAME, - bundleId: IOS_APP_BUNDLE_ID, - appStoreId: IOS_APP_STORE_ID, - }, - } - ); - expect(pollOperationStub).to.be.not.called; - }); - - it("should reject if polling throws error", async () => { - const expectedError = new Error("Permission denied"); - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_1 } }); - pollOperationStub.onFirstCall().rejects(expectedError); - - let err; - try { - await createIosApp(PROJECT_ID, { - displayName: IOS_APP_DISPLAY_NAME, - bundleId: IOS_APP_BUNDLE_ID, - appStoreId: IOS_APP_STORE_ID, - }); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to create iOS app for project ${PROJECT_ID}. See firebase-debug.log for more info.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/iosApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: IOS_APP_DISPLAY_NAME, - bundleId: IOS_APP_BUNDLE_ID, - appStoreId: IOS_APP_STORE_ID, - }, - } - ); - expect(pollOperationStub).to.be.calledOnceWith({ - pollerName: "Create iOS app Poller", - apiOrigin: api.firebaseApiOrigin, - apiVersion: "v1beta1", - operationResourceName: OPERATION_RESOURCE_NAME_1, - }); - }); - }); - - describe("createAndroidApp", () => { - it("should resolve with app data if it succeeds", async () => { - const expectedAppMetadata = { - appId: APP_ID, - displayName: ANDROID_APP_DISPLAY_NAME, - packageName: ANDROID_APP_PACKAGE_NAME, - }; - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_1 } }); - pollOperationStub.onFirstCall().resolves(expectedAppMetadata); - - const resultAppInfo = await createAndroidApp(PROJECT_ID, { - displayName: ANDROID_APP_DISPLAY_NAME, - packageName: ANDROID_APP_PACKAGE_NAME, - }); - - expect(resultAppInfo).to.equal(expectedAppMetadata); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/androidApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: ANDROID_APP_DISPLAY_NAME, - packageName: ANDROID_APP_PACKAGE_NAME, - }, - } - ); - expect(pollOperationStub).to.be.calledOnceWith({ - pollerName: "Create Android app Poller", - apiOrigin: api.firebaseApiOrigin, - apiVersion: "v1beta1", - operationResourceName: OPERATION_RESOURCE_NAME_1, - }); - }); - - it("should reject if app creation api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await createAndroidApp(PROJECT_ID, { - displayName: ANDROID_APP_DISPLAY_NAME, - packageName: ANDROID_APP_PACKAGE_NAME, - }); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to create Android app for project ${PROJECT_ID}. See firebase-debug.log for more info.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/androidApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: ANDROID_APP_DISPLAY_NAME, - packageName: ANDROID_APP_PACKAGE_NAME, - }, - } - ); - expect(pollOperationStub).to.be.not.called; - }); - - it("should reject if polling throws error", async () => { - const expectedError = new Error("Permission denied"); - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_1 } }); - pollOperationStub.onFirstCall().rejects(expectedError); - - let err; - try { - await createAndroidApp(PROJECT_ID, { - displayName: ANDROID_APP_DISPLAY_NAME, - packageName: ANDROID_APP_PACKAGE_NAME, - }); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to create Android app for project ${PROJECT_ID}. See firebase-debug.log for more info.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/androidApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: ANDROID_APP_DISPLAY_NAME, - packageName: ANDROID_APP_PACKAGE_NAME, - }, - } - ); - expect(pollOperationStub).to.be.calledOnceWith({ - pollerName: "Create Android app Poller", - apiOrigin: api.firebaseApiOrigin, - apiVersion: "v1beta1", - operationResourceName: OPERATION_RESOURCE_NAME_1, - }); - }); - }); - - describe("createWebApp", () => { - it("should resolve with app data if it succeeds", async () => { - const expectedAppMetadata = { - appId: APP_ID, - displayName: WEB_APP_DISPLAY_NAME, - }; - apiRequestStub.onFirstCall().resolves({ body: { name: OPERATION_RESOURCE_NAME_1 } }); - pollOperationStub.onFirstCall().resolves(expectedAppMetadata); - - const resultAppInfo = await createWebApp(PROJECT_ID, { displayName: WEB_APP_DISPLAY_NAME }); - - expect(resultAppInfo).to.equal(expectedAppMetadata); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/webApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: WEB_APP_DISPLAY_NAME, - }, - } - ); - expect(pollOperationStub).to.be.calledOnceWith({ - pollerName: "Create Web app Poller", - apiOrigin: api.firebaseApiOrigin, - apiVersion: "v1beta1", - operationResourceName: OPERATION_RESOURCE_NAME_1, - }); - }); - - it("should reject if app creation api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await createWebApp(PROJECT_ID, { displayName: WEB_APP_DISPLAY_NAME }); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to create Web app for project ${PROJECT_ID}. See firebase-debug.log for more info.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/webApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: WEB_APP_DISPLAY_NAME, - }, - } - ); - expect(pollOperationStub).to.be.not.called; - }); - - it("should reject if polling throws error", async () => { - const expectedError = new Error("Permission denied"); - apiRequestStub.onFirstCall().resolves({ - body: { name: OPERATION_RESOURCE_NAME_1 }, - }); - pollOperationStub.onFirstCall().rejects(expectedError); - - let err; - try { - await createWebApp(PROJECT_ID, { displayName: WEB_APP_DISPLAY_NAME }); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to create Web app for project ${PROJECT_ID}. See firebase-debug.log for more info.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta1/projects/${PROJECT_ID}/webApps`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 15000, - data: { - displayName: WEB_APP_DISPLAY_NAME, - }, - } - ); - expect(pollOperationStub).to.be.calledOnceWith({ - pollerName: "Create Web app Poller", - apiOrigin: api.firebaseApiOrigin, - apiVersion: "v1beta1", - operationResourceName: OPERATION_RESOURCE_NAME_1, - }); - }); - }); - - describe("listFirebaseApps", () => { - it("should resolve with app list if it succeeds with only 1 api call", async () => { - const appCountsPerPlatform = 3; - const expectedAppList = [ - ...generateIosAppList(appCountsPerPlatform), - ...generateAndroidAppList(appCountsPerPlatform), - ...generateWebAppList(appCountsPerPlatform), - ]; - apiRequestStub.onFirstCall().resolves({ body: { apps: expectedAppList } }); - - const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.ANY); - - expect(apps).to.deep.equal(expectedAppList); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}:searchApps?pageSize=100`, - { - auth: true, - origin: api.firebaseApiOrigin, - timeout: 30000, - } - ); - }); - - it("should resolve with iOS app list", async () => { - const appCounts = 10; - const expectedAppList = generateIosAppList(appCounts); - const apiResponseAppList = expectedAppList.map((app) => { - const iosApp = { ...app }; - delete iosApp.platform; - return iosApp; - }); - apiRequestStub.onFirstCall().resolves({ body: { apps: apiResponseAppList } }); - - const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.IOS); - - expect(apps).to.deep.equal(expectedAppList); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}/iosApps?pageSize=100` - ); - }); - - it("should resolve with Android app list", async () => { - const appCounts = 10; - const expectedAppList = generateAndroidAppList(appCounts); - const apiResponseAppList = expectedAppList.map((app) => { - const androidApps = { ...app }; - delete androidApps.platform; - return androidApps; - }); - apiRequestStub.onFirstCall().resolves({ body: { apps: apiResponseAppList } }); - - const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.ANDROID); - - expect(apps).to.deep.equal(expectedAppList); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}/androidApps?pageSize=100` - ); - }); - - it("should resolve with Web app list", async () => { - const appCounts = 10; - const expectedAppList = generateWebAppList(appCounts); - const apiResponseAppList = expectedAppList.map((app) => { - const webApp = { ...app }; - delete webApp.platform; - return webApp; - }); - apiRequestStub.onFirstCall().resolves({ body: { apps: apiResponseAppList } }); - - const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.WEB); - - expect(apps).to.deep.equal(expectedAppList); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}/webApps?pageSize=100` - ); - }); - - it("should concatenate pages to get app list if it succeeds", async () => { - const appCountsPerPlatform = 3; - const pageSize = 5; - const nextPageToken = "next-page-token"; - const expectedAppList = [ - ...generateIosAppList(appCountsPerPlatform), - ...generateAndroidAppList(appCountsPerPlatform), - ...generateWebAppList(appCountsPerPlatform), - ]; - apiRequestStub - .onFirstCall() - .resolves({ body: { apps: expectedAppList.slice(0, pageSize), nextPageToken } }) - .onSecondCall() - .resolves({ body: { apps: expectedAppList.slice(pageSize, appCountsPerPlatform * 3) } }); - - const apps = await listFirebaseApps(PROJECT_ID, AppPlatform.ANY, pageSize); - - expect(apps).to.deep.equal(expectedAppList); - expect(apiRequestStub.firstCall).to.be.calledWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}:searchApps?pageSize=${pageSize}` - ); - expect(apiRequestStub.secondCall).to.be.calledWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}:searchApps?pageSize=${pageSize}&pageToken=${nextPageToken}` - ); - }); - - it("should reject if the first api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await listFirebaseApps(PROJECT_ID, AppPlatform.ANY); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - "Failed to list Firebase apps. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}:searchApps?pageSize=100` - ); - }); - - it("should rejects if error is thrown in subsequence api call", async () => { - const appCounts = 10; - const pageSize = 5; - const nextPageToken = "next-page-token"; - const expectedAppList = generateAndroidAppList(appCounts); - const expectedError = new Error("HTTP Error 400: unexpected error"); - apiRequestStub - .onFirstCall() - .resolves({ body: { apps: expectedAppList.slice(0, pageSize), nextPageToken } }) - .onSecondCall() - .rejects(expectedError); - - let err; - try { - await listFirebaseApps(PROJECT_ID, AppPlatform.ANY, pageSize); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - "Failed to list Firebase apps. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub.firstCall).to.be.calledWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}:searchApps?pageSize=${pageSize}` - ); - expect(apiRequestStub.secondCall).to.be.calledWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}:searchApps?pageSize=${pageSize}&pageToken=${nextPageToken}` - ); - }); - - it("should reject if the list iOS apps fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await listFirebaseApps(PROJECT_ID, AppPlatform.IOS); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - "Failed to list Firebase IOS apps. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}/iosApps?pageSize=100` - ); - }); - - it("should reject if the list Android apps fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await listFirebaseApps(PROJECT_ID, AppPlatform.ANDROID); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - "Failed to list Firebase ANDROID apps. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}/androidApps?pageSize=100` - ); - }); - - it("should reject if the list Web apps fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await listFirebaseApps(PROJECT_ID, AppPlatform.WEB); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - "Failed to list Firebase WEB apps. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/${PROJECT_ID}/webApps?pageSize=100` - ); - }); - }); - - describe("getAppConfigFile", () => { - it("should resolve with iOS app configuration if it succeeds", async () => { - const expectedConfigFileContent = "test iOS configuration"; - const mockBase64Content = Buffer.from(expectedConfigFileContent).toString("base64"); - apiRequestStub.onFirstCall().resolves({ - body: { configFilename: "GoogleService-Info.plist", configFileContents: mockBase64Content }, - }); - - const configData = await getAppConfig(APP_ID, AppPlatform.IOS); - const fileData = getAppConfigFile(configData, AppPlatform.IOS); - - expect(fileData).to.deep.equal({ - fileName: "GoogleService-Info.plist", - fileContents: expectedConfigFileContent, - }); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/-/iosApps/${APP_ID}/config` - ); - }); - - it("should resolve with Web app configuration if it succeeds", async () => { - const mockWebConfig = { - projectId: PROJECT_ID, - appId: APP_ID, - apiKey: "api-key", - }; - apiRequestStub.onFirstCall().resolves({ body: mockWebConfig }); - readFileSyncStub.onFirstCall().returns("{/*--CONFIG--*/}"); - - const configData = await getAppConfig(APP_ID, AppPlatform.WEB); - const fileData = getAppConfigFile(configData, AppPlatform.WEB); - - expect(fileData).to.deep.equal({ - fileName: "google-config.js", - fileContents: JSON.stringify(mockWebConfig, null, 2), - }); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/-/webApps/${APP_ID}/config` - ); - expect(readFileSyncStub).to.be.calledOnce; - }); - }); - - describe("getAppConfig", () => { - it("should resolve with iOS app configuration if it succeeds", async () => { - const mockBase64Content = Buffer.from("test iOS configuration").toString("base64"); - apiRequestStub.onFirstCall().resolves({ - body: { configFilename: "GoogleService-Info.plist", configFileContents: mockBase64Content }, - }); - - const configData = await getAppConfig(APP_ID, AppPlatform.IOS); - - expect(configData).to.deep.equal({ - configFilename: "GoogleService-Info.plist", - configFileContents: mockBase64Content, - }); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/-/iosApps/${APP_ID}/config` - ); - }); - - it("should resolve with Android app configuration if it succeeds", async () => { - const mockBase64Content = Buffer.from("test Android configuration").toString("base64"); - apiRequestStub.onFirstCall().resolves({ - body: { configFilename: "google-services.json", configFileContents: mockBase64Content }, - }); - - const configData = await getAppConfig(APP_ID, AppPlatform.ANDROID); - - expect(configData).to.deep.equal({ - configFilename: "google-services.json", - configFileContents: mockBase64Content, - }); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/-/androidApps/${APP_ID}/config` - ); - }); - - it("should resolve with Web app configuration if it succeeds", async () => { - const mockWebConfig = { - projectId: PROJECT_ID, - appId: APP_ID, - apiKey: "api-key", - }; - apiRequestStub.onFirstCall().resolves({ body: mockWebConfig }); - - const configData = await getAppConfig(APP_ID, AppPlatform.WEB); - - expect(configData).to.deep.equal(mockWebConfig); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/-/webApps/${APP_ID}/config` - ); - }); - - it("should reject if api request fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await getAppConfig(APP_ID, AppPlatform.ANDROID); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - "Failed to get ANDROID app configuration. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta1/projects/-/androidApps/${APP_ID}/config` - ); - }); - }); -}); diff --git a/src/test/management/database.spec.ts b/src/test/management/database.spec.ts deleted file mode 100644 index a2b7547cfb4..00000000000 --- a/src/test/management/database.spec.ts +++ /dev/null @@ -1,514 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as api from "../../api"; - -import { - DatabaseLocation, - DatabaseInstance, - DatabaseInstanceType, - DatabaseInstanceState, - getDatabaseInstanceDetails, - createInstance, - listDatabaseInstances, - checkInstanceNameAvailable, -} from "../../management/database"; - -const PROJECT_ID = "the-best-firebase-project"; -const DATABASE_INSTANCE_NAME = "some_instance"; -const SOME_DATABASE_INSTANCE: DatabaseInstance = { - name: DATABASE_INSTANCE_NAME, - location: DatabaseLocation.US_CENTRAL1, - project: PROJECT_ID, - databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.US_CENTRAL1), - type: DatabaseInstanceType.USER_DATABASE, - state: DatabaseInstanceState.ACTIVE, -}; - -const SOME_DATABASE_INSTANCE_EUROPE_WEST1: DatabaseInstance = { - name: DATABASE_INSTANCE_NAME, - location: DatabaseLocation.EUROPE_WEST1, - project: PROJECT_ID, - databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.EUROPE_WEST1), - type: DatabaseInstanceType.USER_DATABASE, - state: DatabaseInstanceState.ACTIVE, -}; - -const INSTANCE_RESPONSE_US_CENTRAL1 = { - name: `projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances/${DATABASE_INSTANCE_NAME}`, - project: PROJECT_ID, - databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.US_CENTRAL1), - type: DatabaseInstanceType.USER_DATABASE, - state: DatabaseInstanceState.ACTIVE, -}; - -const INSTANCE_RESPONSE_EUROPE_WEST1 = { - name: `projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances/${DATABASE_INSTANCE_NAME}`, - project: PROJECT_ID, - databaseUrl: generateDatabaseUrl(DATABASE_INSTANCE_NAME, DatabaseLocation.EUROPE_WEST1), - type: DatabaseInstanceType.USER_DATABASE, - state: DatabaseInstanceState.ACTIVE, -}; - -function generateDatabaseUrl(instanceName: string, location: DatabaseLocation): string { - if (location == DatabaseLocation.ANY) { - throw new Error("can't generate url for any location"); - } - if (location == DatabaseLocation.US_CENTRAL1) { - return `https://${instanceName}.firebaseio.com`; - } - return `https://${instanceName}.${location}.firebasedatabase.app`; -} - -function generateInstanceList(counts: number, location: DatabaseLocation): DatabaseInstance[] { - return Array.from(Array(counts), (_, i: number) => { - const name = `my-db-instance-${i}`; - return { - name: name, - location: location, - project: PROJECT_ID, - databaseUrl: generateDatabaseUrl(name, location), - type: DatabaseInstanceType.USER_DATABASE, - state: DatabaseInstanceState.ACTIVE, - }; - }); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function generateInstanceListApiResponse(counts: number, location: DatabaseLocation): any[] { - return Array.from(Array(counts), (_, i: number) => { - const name = `my-db-instance-${i}`; - return { - name: `projects/${PROJECT_ID}/locations/${location}/instances/${name}`, - project: PROJECT_ID, - databaseUrl: generateDatabaseUrl(name, location), - type: DatabaseInstanceType.USER_DATABASE, - state: DatabaseInstanceState.ACTIVE, - }; - }); -} -describe("Database management", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("getInstanceDetails", () => { - it("should resolve with DatabaseInstance if API call succeeds", async () => { - const expectedDatabaseInstance = SOME_DATABASE_INSTANCE; - apiRequestStub.onFirstCall().resolves({ body: INSTANCE_RESPONSE_US_CENTRAL1 }); - - const resultDatabaseInstance = await getDatabaseInstanceDetails( - PROJECT_ID, - DATABASE_INSTANCE_NAME - ); - - expect(resultDatabaseInstance).to.deep.equal(expectedDatabaseInstance); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/-/instances/${DATABASE_INSTANCE_NAME}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - }); - - it("should reject if API call fails", async () => { - const badInstanceName = "non-existent-instance"; - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - let err; - try { - await getDatabaseInstanceDetails(PROJECT_ID, badInstanceName); - } catch (e) { - err = e; - } - expect(err.message).to.equal( - `Failed to get instance details for instance: ${badInstanceName}. See firebase-debug.log for more details.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/-/instances/${badInstanceName}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - }); - }); - - describe("createInstance", () => { - it("should resolve with new DatabaseInstance if API call succeeds", async () => { - const expectedDatabaseInstance = SOME_DATABASE_INSTANCE_EUROPE_WEST1; - apiRequestStub.onFirstCall().resolves({ body: INSTANCE_RESPONSE_EUROPE_WEST1 }); - const resultDatabaseInstance = await createInstance( - PROJECT_ID, - DATABASE_INSTANCE_NAME, - DatabaseLocation.EUROPE_WEST1, - DatabaseInstanceType.USER_DATABASE - ); - expect(resultDatabaseInstance).to.deep.equal(expectedDatabaseInstance); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances?databaseId=${DATABASE_INSTANCE_NAME}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - data: { - type: DatabaseInstanceType.USER_DATABASE, - }, - } - ); - }); - - it("should reject if API call fails", async () => { - const badInstanceName = "non-existent-instance"; - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await createInstance( - PROJECT_ID, - badInstanceName, - DatabaseLocation.US_CENTRAL1, - DatabaseInstanceType.DEFAULT_DATABASE - ); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to create instance: ${badInstanceName}. See firebase-debug.log for more details.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances?databaseId=${badInstanceName}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - data: { - type: DatabaseInstanceType.DEFAULT_DATABASE, - }, - } - ); - }); - }); - - describe("checkInstanceNameAvailable", () => { - it("should resolve with new DatabaseInstance if specified instance name is available and API call succeeds", async () => { - apiRequestStub.onFirstCall().resolves({ body: INSTANCE_RESPONSE_EUROPE_WEST1 }); - const output = await checkInstanceNameAvailable( - PROJECT_ID, - DATABASE_INSTANCE_NAME, - DatabaseInstanceType.USER_DATABASE, - DatabaseLocation.EUROPE_WEST1 - ); - expect(output).to.deep.equal({ - available: true, - }); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances?databaseId=${DATABASE_INSTANCE_NAME}&validateOnly=true`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - data: { - type: DatabaseInstanceType.USER_DATABASE, - }, - } - ); - }); - - it("should resolve with suggested instance names if the API call fails with suggestions ", async () => { - const badInstanceName = "invalid:database|name"; - const expectedErrorObj = { - context: { - body: { - error: { - details: [ - { - metadata: { - suggested_database_ids: "dbName1,dbName2,dbName3", - }, - }, - ], - }, - }, - }, - }; - apiRequestStub.onFirstCall().rejects(expectedErrorObj); - const output = await checkInstanceNameAvailable( - PROJECT_ID, - badInstanceName, - DatabaseInstanceType.USER_DATABASE, - DatabaseLocation.EUROPE_WEST1 - ); - expect(output).to.deep.equal({ - available: false, - suggestedIds: ["dbName1", "dbName2", "dbName3"], - }); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.EUROPE_WEST1}/instances?databaseId=${badInstanceName}&validateOnly=true`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - data: { - type: DatabaseInstanceType.USER_DATABASE, - }, - } - ); - }); - - it("should reject if API call fails without suggestions", async () => { - const badInstanceName = "non-existent-instance"; - const expectedErrorObj = { - context: { - body: { - error: { - details: [ - { - metadata: {}, - }, - ], - }, - }, - }, - }; - apiRequestStub.onFirstCall().rejects(expectedErrorObj); - - let err; - try { - await checkInstanceNameAvailable( - PROJECT_ID, - badInstanceName, - DatabaseInstanceType.DEFAULT_DATABASE, - DatabaseLocation.US_CENTRAL1 - ); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to validate Realtime Database instance name: ${badInstanceName}.` - ); - expect(err.original).to.equal(expectedErrorObj); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances?databaseId=${badInstanceName}&validateOnly=true`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - data: { - type: DatabaseInstanceType.DEFAULT_DATABASE, - }, - } - ); - }); - }); - - describe("listDatabaseInstances", () => { - it("should resolve with instance list if it succeeds with only 1 api call", async () => { - const instancesPerLocation = 2; - const expectedInstanceList = [ - ...generateInstanceList(instancesPerLocation, DatabaseLocation.US_CENTRAL1), - ...generateInstanceList(instancesPerLocation, DatabaseLocation.EUROPE_WEST1), - ]; - apiRequestStub.onFirstCall().resolves({ - body: { - instances: [ - ...generateInstanceListApiResponse(instancesPerLocation, DatabaseLocation.US_CENTRAL1), - ...generateInstanceListApiResponse(instancesPerLocation, DatabaseLocation.EUROPE_WEST1), - ], - }, - }); - - const instances = await listDatabaseInstances(PROJECT_ID, DatabaseLocation.ANY, 5); - - expect(instances).to.deep.equal(expectedInstanceList); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/-/instances?pageSize=5`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - }); - - it("should resolve with specific location", async () => { - const instancesPerLocation = 2; - const expectedInstancesList = generateInstanceList( - instancesPerLocation, - DatabaseLocation.US_CENTRAL1 - ); - apiRequestStub.onFirstCall().resolves({ - body: { - instances: [ - ...generateInstanceListApiResponse(instancesPerLocation, DatabaseLocation.US_CENTRAL1), - ], - }, - }); - const instances = await listDatabaseInstances(PROJECT_ID, DatabaseLocation.US_CENTRAL1); - - expect(instances).to.deep.equal(expectedInstancesList); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances?pageSize=100`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - }); - - it("should concatenate pages to get instances list if it succeeds", async () => { - const countPerLocation = 3; - const pageSize = 5; - const nextPageToken = "next-page-token"; - const expectedInstancesList = [ - ...generateInstanceList(countPerLocation, DatabaseLocation.US_CENTRAL1), - ...generateInstanceList(countPerLocation, DatabaseLocation.EUROPE_WEST1), - ...generateInstanceList(countPerLocation, DatabaseLocation.EUROPE_WEST1), - ]; - - const expectedResponsesList = [ - ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.US_CENTRAL1), - ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.EUROPE_WEST1), - ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.EUROPE_WEST1), - ]; - - apiRequestStub - .onFirstCall() - .resolves({ - body: { - instances: expectedResponsesList.slice(0, pageSize), - nextPageToken: nextPageToken, - }, - }) - .onSecondCall() - .resolves({ - body: { - instances: expectedResponsesList.slice(pageSize), - }, - }); - - const instances = await listDatabaseInstances(PROJECT_ID, DatabaseLocation.ANY, pageSize); - expect(instances).to.deep.equal(expectedInstancesList); - expect(apiRequestStub.firstCall).to.be.calledWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/-/instances?pageSize=${pageSize}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - expect(apiRequestStub.secondCall).to.be.calledWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/-/instances?pageSize=${pageSize}&pageToken=${nextPageToken}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - }); - - it("should reject if the first api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await listDatabaseInstances(PROJECT_ID, DatabaseLocation.ANY); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - "Failed to list Firebase Realtime Database instances. See firebase-debug.log for more info." - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/-/instances?pageSize=100`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - }); - - it("should reject if error is thrown in subsequent api call", async () => { - const expectedError = new Error("HTTP Error 400: unexpected error"); - const countPerLocation = 5; - const pageSize = 5; - const nextPageToken = "next-page-token"; - - apiRequestStub - .onFirstCall() - .resolves({ - body: { - instances: [ - ...generateInstanceListApiResponse(countPerLocation, DatabaseLocation.US_CENTRAL1), - ].slice(0, pageSize), - nextPageToken: nextPageToken, - }, - }) - .onSecondCall() - .rejects(expectedError); - - let err; - try { - await listDatabaseInstances(PROJECT_ID, DatabaseLocation.US_CENTRAL1, pageSize); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to list Firebase Realtime Database instances for location ${DatabaseLocation.US_CENTRAL1}. See firebase-debug.log for more info.` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub.firstCall).to.be.calledWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances?pageSize=${pageSize}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - expect(apiRequestStub.secondCall).to.be.calledWith( - "GET", - `/v1beta/projects/${PROJECT_ID}/locations/${DatabaseLocation.US_CENTRAL1}/instances?pageSize=${pageSize}&pageToken=${nextPageToken}`, - { - auth: true, - origin: api.rtdbManagementOrigin, - timeout: 10000, - } - ); - }); - }); -}); diff --git a/src/test/profilerReport.spec.js b/src/test/profilerReport.spec.js deleted file mode 100644 index f2a5c519617..00000000000 --- a/src/test/profilerReport.spec.js +++ /dev/null @@ -1,110 +0,0 @@ -"use strict"; - -var chai = require("chai"); - -var path = require("path"); -var stream = require("stream"); -var ProfileReport = require("../profileReport"); - -var expect = chai.expect; - -var combinerFunc = function (obj1, obj2) { - return { count: obj1.count + obj2.count }; -}; - -var fixturesDir = path.resolve(__dirname, "./fixtures"); - -var newReport = function () { - var input = path.resolve(fixturesDir, "profiler-data/sample.json"); - var throwAwayStream = new stream.PassThrough(); - return new ProfileReport(input, throwAwayStream, { - format: "JSON", - isFile: false, - collapse: true, - isInput: true, - }); -}; - -describe("profilerReport", function () { - it("should correctly generate a report", function () { - var report = newReport(); - var output = require(path.resolve(fixturesDir, "profiler-data/sample-output.json")); - return expect(report.generate()).to.eventually.deep.equal(output); - }); - - it("should format numbers correctly", function () { - var result = ProfileReport.formatNumber(5); - expect(result).to.eq("5"); - result = ProfileReport.formatNumber(5.0); - expect(result).to.eq("5"); - result = ProfileReport.formatNumber(3.33); - expect(result).to.eq("3.33"); - result = ProfileReport.formatNumber(3.123423); - expect(result).to.eq("3.12"); - result = ProfileReport.formatNumber(3.129); - expect(result).to.eq("3.13"); - result = ProfileReport.formatNumber(3123423232); - expect(result).to.eq("3,123,423,232"); - result = ProfileReport.formatNumber(3123423232.4242); - expect(result).to.eq("3,123,423,232.42"); - }); - - it("should not collapse paths if not needed", function () { - var report = newReport(); - var data = {}; - for (var i = 0; i < 20; i++) { - data["/path/num" + i] = { count: 1 }; - } - var result = report.collapsePaths(data, combinerFunc); - expect(result).to.deep.eq(data); - }); - - it("should collapse paths to $wildcard", function () { - var report = newReport(); - var data = {}; - for (var i = 0; i < 30; i++) { - data["/path/num" + i] = { count: 1 }; - } - var result = report.collapsePaths(data, combinerFunc); - expect(result).to.deep.eq({ "/path/$wildcard": { count: 30 } }); - }); - - it("should not collapse paths with --no-collapse", function () { - var report = newReport(); - report.options.collapse = false; - var data = {}; - for (var i = 0; i < 30; i++) { - data["/path/num" + i] = { count: 1 }; - } - var result = report.collapsePaths(data, combinerFunc); - expect(result).to.deep.eq(data); - }); - - it("should collapse paths recursively", function () { - var report = newReport(); - var data = {}; - for (var i = 0; i < 30; i++) { - data["/path/num" + i + "/next" + i] = { count: 1 }; - } - data["/path/num1/bar/test"] = { count: 1 }; - data["/foo"] = { count: 1 }; - var result = report.collapsePaths(data, combinerFunc); - expect(result).to.deep.eq({ - "/path/$wildcard/$wildcard": { count: 30 }, - "/path/$wildcard/$wildcard/test": { count: 1 }, - "/foo": { count: 1 }, - }); - }); - - it("should extract the correct path index", function () { - var query = { index: { path: ["foo", "bar"] } }; - var result = ProfileReport.extractReadableIndex(query); - expect(result).to.eq("/foo/bar"); - }); - - it("should extract the correct value index", function () { - var query = { index: {} }; - var result = ProfileReport.extractReadableIndex(query); - expect(result).to.eq(".value"); - }); -}); diff --git a/src/test/remoteconfig/get.spec.ts b/src/test/remoteconfig/get.spec.ts deleted file mode 100644 index fa96bbb3c75..00000000000 --- a/src/test/remoteconfig/get.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as api from "../../api"; -import * as remoteconfig from "../../remoteconfig/get"; -import { RemoteConfigTemplate } from "../../remoteconfig/interfaces"; - -const PROJECT_ID = "the-remoteconfig-test-project"; - -// Test sample template -const expectedProjectInfo: RemoteConfigTemplate = { - conditions: [ - { - name: "RCTestCondition", - expression: "dateTime < dateTime('2020-07-24T00:00:00', 'America/Los_Angeles')", - }, - ], - parameters: { - RCTestkey: { - defaultValue: { - value: "RCTestValue", - }, - }, - }, - version: { - versionNumber: "6", - updateTime: "2020-07-23T17:13:11.190Z", - updateUser: { - email: "abc@gmail.com", - }, - updateOrigin: "CONSOLE", - updateType: "INCREMENTAL_UPDATE", - }, - parameterGroups: { - RCTestCaseGroup: { - parameters: { - RCTestKey2: { - defaultValue: { - value: "RCTestValue2", - }, - description: "This is a test", - }, - }, - }, - }, - etag: "123", -}; - -// Test sample template with two parameters -const projectInfoWithTwoParameters: RemoteConfigTemplate = { - conditions: [ - { - name: "RCTestCondition", - expression: "dateTime < dateTime('2020-07-24T00:00:00', 'America/Los_Angeles')", - }, - ], - parameters: { - RCTestkey: { - defaultValue: { - value: "RCTestValue", - }, - }, - enterNumber: { - defaultValue: { - value: "6", - }, - }, - }, - version: { - versionNumber: "6", - updateTime: "2020-07-23T17:13:11.190Z", - updateUser: { - email: "abc@gmail.com", - }, - updateOrigin: "CONSOLE", - updateType: "INCREMENTAL_UPDATE", - }, - parameterGroups: { - RCTestCaseGroup: { - parameters: { - RCTestKey2: { - defaultValue: { - value: "RCTestValue2", - }, - description: "This is a test", - }, - }, - }, - }, - etag: "123", -}; - -describe("Remote Config GET", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("getTemplate", () => { - it("should return the latest template", async () => { - apiRequestStub.onFirstCall().resolves({ body: expectedProjectInfo }); - - const RCtemplate = await remoteconfig.getTemplate(PROJECT_ID); - - expect(RCtemplate).to.deep.equal(expectedProjectInfo); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1/projects/${PROJECT_ID}/remoteConfig`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - - it("should return the correct version of the template if version is specified", async () => { - apiRequestStub.onFirstCall().resolves({ body: expectedProjectInfo }); - - const RCtemplateVersion = await remoteconfig.getTemplate(PROJECT_ID, "6"); - - expect(RCtemplateVersion).to.deep.equal(expectedProjectInfo); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1/projects/${PROJECT_ID}/remoteConfig?versionNumber=6`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - - it("should return a correctly parsed entry value with one parameter", () => { - const expectRCParameters = "RCTestkey\n"; - const RCParameters = remoteconfig.parseTemplateForTable(expectedProjectInfo.parameters); - - expect(RCParameters).to.deep.equal(expectRCParameters); - }); - - it("should return a correctly parsed entry value with two parameters", () => { - const expectRCParameters = "RCTestkey\nenterNumber\n"; - const RCParameters = remoteconfig.parseTemplateForTable( - projectInfoWithTwoParameters.parameters - ); - - expect(RCParameters).to.deep.equal(expectRCParameters); - }); - - it("should reject if the api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await remoteconfig.getTemplate(PROJECT_ID); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to get Firebase Remote Config template for project ${PROJECT_ID}. ` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1/projects/${PROJECT_ID}/remoteConfig`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - }); -}); diff --git a/src/test/remoteconfig/rollback.spec.ts b/src/test/remoteconfig/rollback.spec.ts deleted file mode 100644 index cc1624e95aa..00000000000 --- a/src/test/remoteconfig/rollback.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { expect } from "chai"; - -import api = require("../../api"); -import sinon = require("sinon"); - -import { RemoteConfigTemplate } from "../../remoteconfig/interfaces"; -import * as remoteconfig from "../../remoteconfig/rollback"; - -const PROJECT_ID = "the-remoteconfig-test-project"; - -function createTemplate( - versionNumber: string, - date: string, - rollbackSource?: string -): RemoteConfigTemplate { - return { - parameterGroups: {}, - version: { - updateUser: { - email: "jackiechu@google.com", - }, - updateTime: date, - updateOrigin: "REST_API", - versionNumber: versionNumber, - rollbackSource: rollbackSource, - }, - conditions: [], - parameters: {}, - etag: "123", - }; -} - -const latestTemplate: RemoteConfigTemplate = createTemplate("115", "2020-08-06T23:11:41.629Z"); -const rollbackTemplate: RemoteConfigTemplate = createTemplate("114", "2020-08-07T23:11:41.629Z"); - -describe("RemoteConfig Rollback", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("rollbackCurrentVersion", () => { - it("should return a rollback to the version number specified", async () => { - apiRequestStub.onFirstCall().resolves({ body: latestTemplate }); - - const RCtemplate = await remoteconfig.rollbackTemplate(PROJECT_ID, 115); - - expect(RCtemplate).to.deep.equal(latestTemplate); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=` + 115, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - - it("should reject invalid rollback version number", async () => { - apiRequestStub.onFirstCall().resolves({ body: latestTemplate }); - - const RCtemplate = await remoteconfig.rollbackTemplate(PROJECT_ID, 1000); - - expect(RCtemplate).to.deep.equal(latestTemplate); - expect(apiRequestStub).to.be.calledOnceWith( - "POST", - `/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=` + 1000, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - try { - await remoteconfig.rollbackTemplate(PROJECT_ID); - } catch (e) { - e; - } - }); - - it("should return a rollback to the previous version", async () => { - apiRequestStub.onFirstCall().resolves({ body: rollbackTemplate }); - - const RCtemplate = await remoteconfig.rollbackTemplate(PROJECT_ID); - - expect(RCtemplate).to.deep.equal(rollbackTemplate); - expect(apiRequestStub).to.be.calledWith( - "POST", - `/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=undefined`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - - it("should reject if the api call fails", async () => { - try { - await remoteconfig.rollbackTemplate(PROJECT_ID); - } catch (e) { - e; - } - - expect(apiRequestStub).to.be.calledWith( - "POST", - `/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=undefined`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - }); -}); diff --git a/src/test/remoteconfig/versionslist.spec.ts b/src/test/remoteconfig/versionslist.spec.ts deleted file mode 100644 index 55bcf06c683..00000000000 --- a/src/test/remoteconfig/versionslist.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as api from "../../api"; -import * as remoteconfig from "../../remoteconfig/versionslist"; -import { ListVersionsResult, Version } from "../../remoteconfig/interfaces"; - -const PROJECT_ID = "the-remoteconfig-test-project"; - -function createVersion(version: string, date: string): Version { - return { - versionNumber: version, - updateTime: date, - updateUser: { email: "jackiechu@google.com" }, - }; -} -// Test template with limit of 2 -const expectedProjectInfoLimit: ListVersionsResult = { - versions: [ - createVersion("114", "2020-07-16T23:22:23.608Z"), - createVersion("113", "2020-06-18T21:10:08.992Z"), - ], -}; - -// Test template with no limit (default template) -const expectedProjectInfoDefault: ListVersionsResult = { - versions: [ - ...expectedProjectInfoLimit.versions, - createVersion("112", "2020-06-16T22:20:34.549Z"), - createVersion("111", "2020-06-16T22:14:24.419Z"), - createVersion("110", "2020-06-16T22:05:03.116Z"), - createVersion("109", "2020-06-16T21:55:19.415Z"), - createVersion("108", "2020-06-16T21:54:55.799Z"), - createVersion("107", "2020-06-16T21:48:37.565Z"), - createVersion("106", "2020-06-16T21:44:41.043Z"), - createVersion("105", "2020-06-16T21:44:13.860Z"), - ], -}; - -// Test template with limit of 0 -const expectedProjectInfoNoLimit: ListVersionsResult = { - versions: [ - ...expectedProjectInfoDefault.versions, - createVersion("104", "2020-06-16T21:39:19.422Z"), - createVersion("103", "2020-06-16T21:37:40.858Z"), - ], -}; - -describe("RemoteConfig ListVersions", () => { - let sandbox: sinon.SinonSandbox; - let apiRequestStub: sinon.SinonStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - apiRequestStub = sandbox.stub(api, "request").throws("Unexpected API request call"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("getVersionTemplate", () => { - it("should return the list of versions up to the limit", async () => { - apiRequestStub.onFirstCall().resolves({ body: expectedProjectInfoLimit }); - - const RCtemplate = await remoteconfig.getVersions(PROJECT_ID, 2); - - expect(RCtemplate).to.deep.equal(expectedProjectInfoLimit); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=` + 2, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - - it("should return all the versions when the limit is 0", async () => { - apiRequestStub.onFirstCall().resolves({ body: expectedProjectInfoNoLimit }); - - const RCtemplate = await remoteconfig.getVersions(PROJECT_ID, 0); - - expect(RCtemplate).to.deep.equal(expectedProjectInfoNoLimit); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=` + 300, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - - it("should return with default 10 versions when no limit is set", async () => { - apiRequestStub.onFirstCall().resolves({ body: expectedProjectInfoDefault }); - - const RCtemplateVersion = await remoteconfig.getVersions(PROJECT_ID); - - expect(RCtemplateVersion.versions.length).to.deep.equal(10); - expect(RCtemplateVersion).to.deep.equal(expectedProjectInfoDefault); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=` + 10, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - - it("should reject if the api call fails", async () => { - const expectedError = new Error("HTTP Error 404: Not Found"); - apiRequestStub.onFirstCall().rejects(expectedError); - - let err; - try { - await remoteconfig.getVersions(PROJECT_ID); - } catch (e) { - err = e; - } - - expect(err.message).to.equal( - `Failed to get Remote Config template versions for Firebase project ${PROJECT_ID}. ` - ); - expect(err.original).to.equal(expectedError); - expect(apiRequestStub).to.be.calledOnceWith( - "GET", - `/v1/projects/${PROJECT_ID}/remoteConfig:listVersions?pageSize=10`, - { - auth: true, - origin: api.remoteConfigApiOrigin, - timeout: 30000, - } - ); - }); - }); -}); diff --git a/src/test/utils.spec.ts b/src/test/utils.spec.ts deleted file mode 100644 index e2bb07dfe82..00000000000 --- a/src/test/utils.spec.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { expect } from "chai"; - -import * as utils from "../utils"; - -describe("utils", () => { - describe("consoleUrl", () => { - it("should create a console URL", () => { - expect(utils.consoleUrl("projectId", "/foo/bar")).to.equal( - "https://console.firebase.google.com/project/projectId/foo/bar" - ); - }); - }); - - describe("getInheritedOption", () => { - it("should chain up looking for a key", () => { - const o1 = {}; - const o2 = { parent: o1, foo: "bar" }; - const o3 = { parent: o2, bar: "foo" }; - const o4 = { parent: o3, baz: "zip" }; - - expect(utils.getInheritedOption(o4, "foo")).to.equal("bar"); - }); - - it("should return undefined if the key does not exist", () => { - const o1 = {}; - const o2 = { parent: o1, foo: "bar" }; - const o3 = { parent: o2, bar: "foo" }; - const o4 = { parent: o3, baz: "zip" }; - - expect(utils.getInheritedOption(o4, "zip")).to.equal(undefined); - }); - }); - - describe("envOverride", () => { - it("should return the value if no current value exists", () => { - expect(utils.envOverride("FOOBARBAZ", "notset")).to.equal("notset"); - }); - - it("should set an override if it conflicts", () => { - process.env.FOO_BAR_BAZ = "set"; - - expect(utils.envOverride("FOO_BAR_BAZ", "notset")).to.equal("set"); - expect(utils.envOverrides).to.contain("FOO_BAR_BAZ"); - - delete process.env.FOO_BAR_BAZ; - }); - - it("should coerce the value", () => { - process.env.FOO_BAR_BAZ = "set"; - - expect(utils.envOverride("FOO_BAR_BAZ", "notset", (s) => s.split(""))).to.deep.equal([ - "s", - "e", - "t", - ]); - - delete process.env.FOO_BAR_BAZ; - }); - - it("should return provided value if coerce fails", () => { - process.env.FOO_BAR_BAZ = "set"; - - const coerce = () => { - throw new Error(); - }; - expect(utils.envOverride("FOO_BAR_BAZ", "notset", coerce)).to.deep.equal("notset"); - - delete process.env.FOO_BAR_BAZ; - }); - }); - - describe("getDatabaseUrl", () => { - it("should create a url for prod", () => { - expect(utils.getDatabaseUrl("https://firebaseio.com", "fir-proj", "/")).to.equal( - "https://fir-proj.firebaseio.com/" - ); - expect(utils.getDatabaseUrl("https://firebaseio.com", "fir-proj", "/foo/bar")).to.equal( - "https://fir-proj.firebaseio.com/foo/bar" - ); - expect(utils.getDatabaseUrl("https://firebaseio.com", "fir-proj", "/foo/bar.json")).to.equal( - "https://fir-proj.firebaseio.com/foo/bar.json" - ); - expect( - utils.getDatabaseUrl( - "https://some-namespace.europe-west1.firebasedatabase.app", - "some-namespace", - "/foo/bar.json" - ) - ).to.equal("https://some-namespace.europe-west1.firebasedatabase.app/foo/bar.json"); - expect( - utils.getDatabaseUrl( - "https://europe-west1.firebasedatabase.app", - "some-namespace", - "/foo/bar.json" - ) - ).to.equal("https://some-namespace.europe-west1.firebasedatabase.app/foo/bar.json"); - }); - - it("should create a url for the emulator", () => { - expect(utils.getDatabaseUrl("http://localhost:9000", "fir-proj", "/")).to.equal( - "http://localhost:9000/?ns=fir-proj" - ); - expect(utils.getDatabaseUrl("http://localhost:9000", "fir-proj", "/foo/bar")).to.equal( - "http://localhost:9000/foo/bar?ns=fir-proj" - ); - expect(utils.getDatabaseUrl("http://localhost:9000", "fir-proj", "/foo/bar.json")).to.equal( - "http://localhost:9000/foo/bar.json?ns=fir-proj" - ); - }); - }); - - describe("getDatabaseViewDataUrl", () => { - it("should get a view data url for legacy prod URL", () => { - expect( - utils.getDatabaseViewDataUrl("https://firebaseio.com", "fir-proj", "fir-ns", "/foo/bar") - ).to.equal( - "https://console.firebase.google.com/project/fir-proj/database/fir-ns/data/foo/bar" - ); - }); - - it("should get a view data url for new prod URL", () => { - expect( - utils.getDatabaseViewDataUrl( - "https://firebasedatabase.app", - "fir-proj", - "fir-ns", - "/foo/bar" - ) - ).to.equal( - "https://console.firebase.google.com/project/fir-proj/database/fir-ns/data/foo/bar" - ); - }); - - it("should get a view data url for the emulator", () => { - expect( - utils.getDatabaseViewDataUrl("http://localhost:9000", "fir-proj", "fir-ns", "/foo/bar") - ).to.equal("http://localhost:9000/foo/bar.json?ns=fir-ns"); - }); - }); - - describe("addDatabaseNamespace", () => { - it("should add the namespace for prod", () => { - expect(utils.addDatabaseNamespace("https://firebaseio.com/", "fir-proj")).to.equal( - "https://fir-proj.firebaseio.com/" - ); - expect(utils.addDatabaseNamespace("https://firebaseio.com/foo/bar", "fir-proj")).to.equal( - "https://fir-proj.firebaseio.com/foo/bar" - ); - }); - - it("should add the namespace for the emulator", () => { - expect(utils.addDatabaseNamespace("http://localhost:9000/", "fir-proj")).to.equal( - "http://localhost:9000/?ns=fir-proj" - ); - expect(utils.addDatabaseNamespace("http://localhost:9000/foo/bar", "fir-proj")).to.equal( - "http://localhost:9000/foo/bar?ns=fir-proj" - ); - }); - }); - - describe("addSubdomain", () => { - it("should add a subdomain", () => { - expect(utils.addSubdomain("https://example.com", "sub")).to.equal("https://sub.example.com"); - }); - }); - - describe("endpoint", () => { - it("should join our strings", () => { - expect(utils.endpoint(["foo", "bar"])).to.equal("/foo/bar"); - }); - }); - - describe("promiseAllSettled", () => { - it("should settle all promises", async () => { - const result = await utils.promiseAllSettled([ - Promise.resolve("foo"), - Promise.reject("bar"), - Promise.resolve("baz"), - ]); - expect(result).to.deep.equal([ - { state: "fulfilled", value: "foo" }, - { state: "rejected", reason: "bar" }, - { state: "fulfilled", value: "baz" }, - ]); - }); - }); - - describe("promiseProps", () => { - it("should resolve all promises", async () => { - const o = { - foo: new Promise((resolve) => { - setTimeout(() => { - resolve("1"); - }); - }), - bar: Promise.resolve("2"), - }; - - const result = await utils.promiseProps(o); - expect(result).to.deep.equal({ - foo: "1", - bar: "2", - }); - }); - - it("should pass through objects", async () => { - const o = { - foo: new Promise((resolve) => { - setTimeout(() => { - resolve("1"); - }); - }), - bar: ["bar"], - }; - - const result = await utils.promiseProps(o); - expect(result).to.deep.equal({ - foo: "1", - bar: ["bar"], - }); - }); - - it("should reject if a promise rejects", async () => { - const o = { - foo: new Promise((_, reject) => { - setTimeout(() => { - reject(new Error("1")); - }); - }), - bar: Promise.resolve("2"), - }; - - return expect(utils.promiseProps(o)).to.eventually.be.rejected; - }); - }); - - describe("datetimeString", () => { - it("should output the date in the correct format", () => { - // Don't worry about the hour since timezones screw everything up. - expect(utils.datetimeString(new Date("February 22, 2020 11:35:45-07:00"))).to.match( - /^2020-02-22 \d\d:35:45$/ - ); - expect(utils.datetimeString(new Date("February 7, 2020 11:35:45-07:00"))).to.match( - /^2020-02-07 \d\d:35:45$/ - ); - expect(utils.datetimeString(new Date("February 7, 2020 8:01:01-07:00"))).to.match( - /^2020-02-07 \d\d:01:01$/ - ); - }); - }); - - describe("streamToString/stringToStream", () => { - it("should be able to create and read streams", async () => { - const stream = utils.stringToStream("hello world"); - if (!stream) { - throw new Error("stream came back undefined"); - } - await expect(utils.streamToString(stream)).to.eventually.equal("hello world"); - }); - }); - - describe("allSettled", () => { - it("handles arrays of length zero", async () => { - const res = await utils.allSettled([]); - expect(res).to.deep.equal([]); - }); - - it("handles a simple success", async () => { - const res = await utils.allSettled([Promise.resolve(42)]); - expect(res).to.deep.equal([ - { - status: "fulfilled", - value: 42, - }, - ]); - }); - - it("handles a simple failure", async () => { - const res = await utils.allSettled([Promise.reject(new Error("oh noes!"))]); - expect(res.length).to.equal(1); - expect(res[0].status).to.equal("rejected"); - expect((res[0] as utils.PromiseRejectedResult).reason).to.be.instanceOf(Error); - expect((res[0] as any).reason?.message).to.equal("oh noes!"); - }); - - it("waits for all settled", async () => { - // Intetionally failing with a non-error to make matching easier - const reject = Promise.reject("fail fast"); - const resolve = new Promise((res) => { - setTimeout(() => res(42), 20); - }); - - const results = await utils.allSettled([reject, resolve]); - expect(results).to.deep.equal([ - { status: "rejected", reason: "fail fast" }, - { status: "fulfilled", value: 42 }, - ]); - }); - }); -}); diff --git a/src/throttler/errors/retries-exhausted-error.ts b/src/throttler/errors/retries-exhausted-error.ts index 1704d14a7a4..aa1f59414ca 100644 --- a/src/throttler/errors/retries-exhausted-error.ts +++ b/src/throttler/errors/retries-exhausted-error.ts @@ -7,7 +7,7 @@ export default class RetriesExhaustedError extends TaskError { `retries exhausted after ${totalRetries + 1} attempts, with error: ${lastTrialError.message}`, { original: lastTrialError, - } + }, ); } } diff --git a/src/test/throttler/queue.spec.ts b/src/throttler/queue.spec.ts similarity index 94% rename from src/test/throttler/queue.spec.ts rename to src/throttler/queue.spec.ts index 6b0659c23e4..8d3b93f8415 100644 --- a/src/test/throttler/queue.spec.ts +++ b/src/throttler/queue.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; -import Queue from "../../throttler/queue"; +import Queue from "./queue"; import { createHandler, createTask, Task } from "./throttler.spec"; describe("Queue", () => { diff --git a/src/test/throttler/stack.spec.ts b/src/throttler/stack.spec.ts similarity index 97% rename from src/test/throttler/stack.spec.ts rename to src/throttler/stack.spec.ts index 311ca79e090..97afa5e8830 100644 --- a/src/test/throttler/stack.spec.ts +++ b/src/throttler/stack.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; -import Stack from "../../throttler/stack"; +import Stack from "./stack"; import { createHandler, createTask, Task } from "./throttler.spec"; describe("Stack", () => { diff --git a/src/test/throttler/throttler.spec.ts b/src/throttler/throttler.spec.ts similarity index 94% rename from src/test/throttler/throttler.spec.ts rename to src/throttler/throttler.spec.ts index 86429ec3089..9328f9a6519 100644 --- a/src/test/throttler/throttler.spec.ts +++ b/src/throttler/throttler.spec.ts @@ -1,12 +1,12 @@ import * as sinon from "sinon"; import { expect } from "chai"; -import Queue from "../../throttler/queue"; -import Stack from "../../throttler/stack"; -import { Throttler, ThrottlerOptions, timeToWait } from "../../throttler/throttler"; -import TaskError from "../../throttler/errors/task-error"; -import TimeoutError from "../../throttler/errors/timeout-error"; -import RetriesExhaustedError from "../../throttler/errors/retries-exhausted-error"; +import Queue from "./queue"; +import Stack from "./stack"; +import { Throttler, ThrottlerOptions, timeToWait } from "./throttler"; +import TaskError from "./errors/task-error"; +import TimeoutError from "./errors/timeout-error"; +import RetriesExhaustedError from "./errors/retries-exhausted-error"; const TEST_ERROR = new Error("foobar"); @@ -110,7 +110,7 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => expect(err).to.be.an.instanceof(RetriesExhaustedError); expect(err.original).to.equal(TEST_ERROR); expect(err.message).to.equal( - "Task index 0 failed: retries exhausted after 1 attempts, with error: foobar" + "Task index 0 failed: retries exhausted after 1 attempts, with error: foobar", ); }) .then(() => { @@ -143,7 +143,7 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => expect(err).to.be.an.instanceof(RetriesExhaustedError); expect(err.original).to.equal(TEST_ERROR); expect(err.message).to.equal( - "Task index 0 failed: retries exhausted after 4 attempts, with error: foobar" + "Task index 0 failed: retries exhausted after 4 attempts, with error: foobar", ); }) .then(() => { @@ -186,7 +186,7 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => return q .wait() .catch((err: Error) => { - throw new Error("handler should have passed "); + throw new Error(`handler should have passed ${err.message}`); }) .then(() => { expect(q.complete).to.equal(3); @@ -223,7 +223,7 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => return q .wait() .catch((err: Error) => { - throw new Error("handler should have passed"); + throw new Error(`handler should have passed ${err.message}`); }) .then(() => { expect(handler.callCount).to.equal(9); @@ -280,7 +280,7 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => let err; try { await q.run(2, 100); - } catch (e) { + } catch (e: any) { err = e; } expect(err).to.be.instanceOf(TimeoutError); @@ -299,13 +299,13 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => let err; try { await q.run(2, 200); - } catch (e) { + } catch (e: any) { err = e; } expect(err).to.be.instanceOf(RetriesExhaustedError); expect(err.original).to.equal(TEST_ERROR); expect(err.message).to.equal( - "Task index 0 failed: retries exhausted after 3 attempts, with error: foobar" + "Task index 0 failed: retries exhausted after 3 attempts, with error: foobar", ); expect(handler.callCount).to.equal(3); expect(q.complete).to.equal(1); @@ -327,7 +327,7 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => let err; try { await q.run(2, 100); - } catch (e) { + } catch (e: any) { err = e; } expect(err).to.be.instanceOf(TimeoutError); @@ -357,7 +357,7 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => let err; try { await q.wait(); - } catch (e) { + } catch (e: any) { err = e; } expect(err).to.be.instanceOf(TimeoutError); @@ -385,12 +385,12 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => let err; try { await q.wait(); - } catch (e) { + } catch (e: any) { err = e; } expect(err).to.be.instanceOf(RetriesExhaustedError); expect(err.message).to.equal( - "Task index 1 failed: retries exhausted after 2 attempts, with error: foobar" + "Task index 1 failed: retries exhausted after 2 attempts, with error: foobar", ); expect(handler.callCount).to.equal(3); expect(q.complete).to.equal(2); @@ -485,7 +485,7 @@ export const createTask = (name: string, resolved: boolean) => { resolve = s; reject = j; }); - const startExecutePromise = new Promise((s, j) => { + const startExecutePromise = new Promise((s) => { startExecute = s; }); res({ diff --git a/src/throttler/throttler.ts b/src/throttler/throttler.ts index 452d70ab8a1..de098dafd20 100644 --- a/src/throttler/throttler.ts +++ b/src/throttler/throttler.ts @@ -3,13 +3,19 @@ import RetriesExhaustedError from "./errors/retries-exhausted-error"; import TimeoutError from "./errors/timeout-error"; import TaskError from "./errors/task-error"; -function backoff(retryNumber: number, delay: number, maxDelay: number): Promise { +/** + * Creates a promise to wait for the nth backoff. + */ +export function backoff(retryNumber: number, delay: number, maxDelay: number): Promise { return new Promise((resolve: () => void) => { setTimeout(resolve, timeToWait(retryNumber, delay, maxDelay)); }); } // Exported for unit testing. +/** + * time to wait between backoffs + */ export function timeToWait(retryNumber: number, delay: number, maxDelay: number): number { return Math.min(delay * Math.pow(2, retryNumber), maxDelay); } @@ -43,7 +49,7 @@ export interface ThrottlerStats { interface TaskData { task: T; retryCount: number; - wait?: { resolve: (R: any) => void; reject: (err: TaskError) => void }; + wait?: { resolve: (value: R) => void; reject: (err: TaskError) => void }; timeoutMillis?: number; timeoutId?: NodeJS.Timeout; isTimedOut: boolean; @@ -58,26 +64,26 @@ interface TaskData { * 2. Not specify the handler, but T must be () => R. */ export abstract class Throttler { - name: string = ""; - concurrency: number = 200; + name = ""; + concurrency = 200; handler: (task: T) => Promise = DEFAULT_HANDLER; - active: number = 0; - complete: number = 0; - success: number = 0; - errored: number = 0; - retried: number = 0; - total: number = 0; + active = 0; + complete = 0; + success = 0; + errored = 0; + retried = 0; + total = 0; taskDataMap = new Map>(); waits: Array<{ resolve: () => void; reject: (err: Error) => void }> = []; - min: number = 9999999999; - max: number = 0; - avg: number = 0; - retries: number = 0; - backoff: number = 200; - maxBackoff: number = 60000; // 1 minute - closed: boolean = false; - finished: boolean = false; - startTime: number = 0; + min = 9999999999; + max = 0; + avg = 0; + retries = 0; + backoff = 200; + maxBackoff = 60000; // 1 minute + closed = false; + finished = false; + startTime = 0; constructor(options: ThrottlerOptions) { if (options.name) { @@ -170,7 +176,7 @@ export abstract class Throttler { let result; try { result = await Promise.race(promises); - } catch (err) { + } catch (err: any) { this.errored++; this.complete++; this.active--; @@ -209,7 +215,7 @@ export abstract class Throttler { private addHelper( task: T, timeoutMillis?: number, - wait?: { resolve: (result: R) => void; reject: (err: Error) => void } + wait?: { resolve: (result: R) => void; reject: (err: Error) => void }, ): void { if (this.closed) { throw new Error("Cannot add a task to a closed throttler."); @@ -266,7 +272,7 @@ export abstract class Throttler { let result; try { result = await this.handler(taskData.task); - } catch (err) { + } catch (err: any) { if (taskData.retryCount === this.retries) { throw new RetriesExhaustedError(this.taskName(cursorIndex), this.retries, err); } diff --git a/src/track.js b/src/track.js deleted file mode 100644 index d52a298d71f..00000000000 --- a/src/track.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict"; - -var ua = require("universal-analytics"); - -var _ = require("lodash"); -var { configstore } = require("./configstore"); -var pkg = require("../package.json"); -var uuid = require("uuid"); -const { logger } = require("./logger"); - -var anonId = configstore.get("analytics-uuid"); -if (!anonId) { - anonId = uuid.v4(); - configstore.set("analytics-uuid", anonId); -} - -var visitor = ua(process.env.FIREBASE_ANALYTICS_UA || "UA-29174744-3", anonId, { - strictCidFormat: false, - https: true, -}); - -visitor.set("cd1", process.platform); // Platform -visitor.set("cd2", process.version); // NodeVersion -visitor.set("cd3", process.env.FIREPIT_VERSION || "none"); // FirepitVersion - -function track(action, label, duration) { - return new Promise(function (resolve) { - if (!_.isString(action) || !_.isString(label)) { - logger.debug("track received non-string arguments:", action, label); - resolve(); - } - duration = duration || 0; - - if (configstore.get("tokens") && configstore.get("usage")) { - visitor.event("Firebase CLI " + pkg.version, action, label, duration).send(function () { - // we could handle errors here, but we won't - resolve(); - }); - } else { - resolve(); - } - }); -} - -// New code should import track by name so that it can be stubbed -// in unit tests. Legacy code still imports as default. -track.track = track; -module.exports = track; diff --git a/src/track.ts b/src/track.ts new file mode 100644 index 00000000000..6dfd947f3a7 --- /dev/null +++ b/src/track.ts @@ -0,0 +1,408 @@ +import fetch from "node-fetch"; +import * as ua from "universal-analytics"; +import { v4 as uuidV4 } from "uuid"; +import { getGlobalDefaultAccount } from "./auth"; + +import { configstore } from "./configstore"; +import { logger } from "./logger"; +const pkg = require("../package.json"); + +type cliEventNames = + | "command_execution" + | "product_deploy" + | "error" + | "login" + | "api_enabled" + | "hosting_version" + | "extension_added_to_manifest" + | "extensions_deploy" + | "extensions_emulated" + | "function_deploy" + | "codebase_deploy" + | "function_deploy_group"; +type GA4Property = "cli" | "emulator" | "vscode"; +interface GA4Info { + measurementId: string; + apiSecret: string; + clientIdKey: string; + currentSession?: AnalyticsSession; +} +export const GA4_PROPERTIES: Record = { + // Info for the GA4 property for the rest of the CLI. + cli: { + measurementId: process.env.FIREBASE_CLI_GA4_MEASUREMENT_ID || "G-PDN0QWHQJR", + apiSecret: process.env.FIREBASE_CLI_GA4_API_SECRET || "LSw5lNxhSFSWeB6aIzJS2w", + clientIdKey: "analytics-uuid", + }, + // Info for the GA4 property for the Emulator Suite only. Should only + // be used in Emulator UI and emulator-related commands (e.g. emulators:start). + emulator: { + measurementId: process.env.FIREBASE_EMULATOR_GA4_MEASUREMENT_ID || "G-KYP2JMPFC0", + apiSecret: process.env.FIREBASE_EMULATOR_GA4_API_SECRET || "2V_zBYc4TdeoppzDaIu0zw", + clientIdKey: "emulator-analytics-clientId", + }, + // Info for the GA4 property for the VSCode Extension only. + vscode: { + measurementId: process.env.FIREBASE_VSCODE_GA4_MEASUREMENT_ID || "G-FYJ489XM2T", + apiSecret: process.env.FIREBASE_VSCODE_GA4_API_SECRET || "XAEWKHe7RM-ygCK44N52Ww", + clientIdKey: "vscode-analytics-clientId", + }, +}; +/** + * UA is enabled only if: + * 1) Entrypoint to the code is Firebase CLI (not require("firebase-tools")). + * 2) User opted-in. + */ +export function usageEnabled(): boolean { + return !!process.env.IS_FIREBASE_CLI && !!configstore.get("usage"); +} + +// Prop name length must <= 24 and cannot begin with google_/ga_/firebase_. +// https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=firebase#reserved_parameter_names +const GA4_USER_PROPS = { + node_platform: { + value: process.platform, + }, + node_version: { + value: process.version, + }, + cli_version: { + value: pkg.version, + }, + firepit_version: { + value: process.env.FIREPIT_VERSION || "none", + }, +}; + +export interface AnalyticsParams { + /** The command running right now (param for custom dimension) */ + command_name?: string; + + /** The emulator related to the event (param for custom dimension) */ + emulator_name?: string; + + /** The number of times or objects (param for custom metrics) */ + count?: number; + + /** The elapsed time in milliseconds (e.g. for command runs) (param for custom metrics) */ + duration?: number; + + /** The result (success or error) of a command */ + result?: string; + + /** Whether the command was run in interactive or noninteractive mode */ + interactive?: string; + /** + * One-off params (that may be used for custom params / metrics later). + * + * Custom parameter names should be in snake_case. (Formal requirement: + * length <= 40, alpha-numeric characters and underscores only (*no spaces*), + * and must start with an alphabetic character.) + * + * If the value is a string, it must have length <= 100. For convenience, the + * entire paramater is omitted (not sent to GA4) if value is set to undefined. + */ + [key: string]: string | number | undefined; +} + +export async function trackGA4( + eventName: cliEventNames, + params: AnalyticsParams, + duration: number = 1, // Default to 1ms duration so that events show up in realtime view. +): Promise { + const session = cliSession(); + if (!session) { + return; + } + return _ga4Track({ + session, + apiSecret: GA4_PROPERTIES.cli.apiSecret, + eventName, + params, + duration, + }); +} + +/** + * Record an emulator-related event for Analytics. + * + * @param eventName the event name in snake_case. (Formal requirement: + * length <= 40, alpha-numeric characters and underscores only + * (*no spaces*), and must start with an alphabetic character) + * @param params custom and standard parameters attached to the event + * @return a Promise fulfilled when the event reaches the server or fails + * (never rejects unless `emulatorSession().validateOnly` is set) + * + * Note: On performance or latency critical paths, the returned Promise may be + * safely ignored with the statement `void trackEmulator(...)`. + */ +export async function trackEmulator(eventName: string, params?: AnalyticsParams): Promise { + const session = emulatorSession(); + if (!session) { + return; + } + + // Since there's no concept of foreground / active, we'll just assume users + // are constantly engaging with the CLI since Node.js process started. (Yes, + // staring at the terminal and waiting for the command to finish also counts.) + const oldTotalEngagementSeconds = session.totalEngagementSeconds; + session.totalEngagementSeconds = process.uptime(); + const duration = session.totalEngagementSeconds - oldTotalEngagementSeconds; + return _ga4Track({ + session, + apiSecret: GA4_PROPERTIES.emulator.apiSecret, + eventName, + params, + duration, + }); +} + +/** + * Record a vscode-related event for Analytics. + * + * @param eventName the event name in snake_case. (Formal requirement: + * length <= 40, alpha-numeric characters and underscores only + * (*no spaces*), and must start with an alphabetic character) + * @param params custom and standard parameters attached to the event + * @return a Promise fulfilled when the event reaches the server or fails + * + * Note: On performance or latency critical paths, the returned Promise may be + * safely ignored with the statement `void trackVSCode(...)`. + */ +export async function trackVSCode(eventName: string, params?: AnalyticsParams): Promise { + const session = vscodeSession(); + if (!session) { + return; + } + + session.debugMode = process.env.VSCODE_DEBUG_MODE === "true"; + + const oldTotalEngagementSeconds = session.totalEngagementSeconds; + session.totalEngagementSeconds = process.uptime(); + const duration = session.totalEngagementSeconds - oldTotalEngagementSeconds; + return _ga4Track({ + session, + apiSecret: GA4_PROPERTIES.vscode.apiSecret, + eventName, + params, + duration, + }); +} + +async function _ga4Track(args: { + session: AnalyticsSession; + apiSecret: string; + eventName: string; + params?: AnalyticsParams; + duration?: number; +}): Promise { + const { session, apiSecret, eventName, params, duration } = args; + + // Memorize and set command_name throughout the session. + session.commandName = params?.command_name || session.commandName; + + const search = `?api_secret=${apiSecret}&measurement_id=${session.measurementId}`; + const validate = session.validateOnly ? "debug/" : ""; + const url = `https://www.google-analytics.com/${validate}mp/collect${search}`; + const body = { + // Get timestamp in millis and append '000' to get micros as string. + // Not using multiplication due to JS number precision limit. + timestamp_micros: `${Date.now()}000`, + client_id: session.clientId, + user_properties: { + ...GA4_USER_PROPS, + java_major_version: session.javaMajorVersion + ? { value: session.javaMajorVersion } + : undefined, + }, + validationBehavior: session.validateOnly ? "ENFORCE_RECOMMENDATIONS" : undefined, + events: [ + { + name: eventName, + params: { + session_id: session.sessionId, + + // engagement_time_msec and session_id must be set for the activity + // to display in standard reports like Realtime. + // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#optional_parameters_for_reports + + // https://support.google.com/analytics/answer/11109416?hl=en + // Additional engagement time since last event, in microseconds. + engagement_time_msec: (duration ?? 0).toFixed(3).replace(".", "").replace(/^0+/, ""), // trim leading zeros + + // https://support.google.com/analytics/answer/7201382?hl=en + // To turn debug mode off, `debug_mode` must be left out not `false`. + debug_mode: session.debugMode ? true : undefined, + command_name: session.commandName, + ...params, + }, + }, + ], + }; + if (session.validateOnly) { + logger.info( + `Sending Analytics for event ${eventName} to property ${session.measurementId}`, + params, + body, + ); + } + try { + const response = await fetch(url, { + method: "POST", + headers: { + "content-type": "application/json;charset=UTF-8", + }, + body: JSON.stringify(body), + }); + if (session.validateOnly) { + // If the validation endpoint is used, response may contain errors. + if (!response.ok) { + logger.warn(`Analytics validation HTTP error: ${response.status}`); + } + const respBody = await response.text(); + logger.info(`Analytics validation result: ${respBody}`); + } + // response.ok / response.status intentionally ignored, see comment below. + } catch (e: unknown) { + if (session.validateOnly) { + throw e; + } + // Otherwise, we will ignore the status / error for these reasons: + // * the endpoint always return 2xx even if request is malformed + // * non-2xx requests should _not_ be retried according to documentation + // * analytics is non-critical and should not fail other operations. + // https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#response_codes + return; + } +} + +export interface AnalyticsSession { + measurementId: string; + clientId: string; + + // https://support.google.com/analytics/answer/9191807 + // We treat each CLI invocation as a different session, including any CLI + // events and Emulator UI interactions. + sessionId: string; + totalEngagementSeconds: number; + + // Whether the events sent should be tagged so that they are shown in GA Debug + // View in real time (for Googler to debug) and excluded from reports. + debugMode: boolean; + + // Whether to validate events format instead of collecting them. Should only + // be used to debug the Firebase CLI / Emulator UI itself regarding issues + // with Analytics. To enable, set the env var FIREBASE_CLI_MP_VALIDATE. + // In the CLI, this is implemented by sending events to the GA4 measurement + // validation API (which does not persist events) and printing the response. + validateOnly: boolean; + + // The Java major version, if known. Will be attached to subsequent events. + javaMajorVersion?: number; + + commandName?: string; +} + +export function emulatorSession(): AnalyticsSession | undefined { + return session("emulator"); +} + +export function vscodeSession(): AnalyticsSession | undefined { + return session("vscode"); +} + +export function cliSession(): AnalyticsSession | undefined { + return session("cli"); +} + +function session(propertyName: GA4Property): AnalyticsSession | undefined { + const validateOnly = !!process.env.FIREBASE_CLI_MP_VALIDATE; + if (!usageEnabled() && propertyName !== "vscode") { + if (validateOnly) { + logger.warn("Google Analytics is DISABLED. To enable, (re)login and opt in to collection."); + } + return; + } + const property = GA4_PROPERTIES[propertyName]; + if (!property.currentSession) { + let clientId: string | undefined = configstore.get(property.clientIdKey); + if (!clientId) { + clientId = uuidV4(); + configstore.set(property.clientIdKey, clientId); + } + property.currentSession = { + measurementId: property.measurementId, + clientId, + + // This must be an int64 string, but only ~50 bits are generated here + // for simplicity. (AFAICT, they just need to be unique per clientId, + // instead of globally. Revisit if that is not the case.) + // https://help.analyticsedge.com/article/misunderstood-metrics-sessions-in-google-analytics-4/#:~:text=The%20Session%20ID%20Is%20Not%20Unique + sessionId: (Math.random() * Number.MAX_SAFE_INTEGER).toFixed(0), + totalEngagementSeconds: 0, + debugMode: isDebugMode(), + validateOnly, + }; + } + return property.currentSession; +} + +function isDebugMode(): boolean { + const account = getGlobalDefaultAccount(); + if (account?.user.email.endsWith("@google.com")) { + try { + require("../tsconfig.json"); + logger.info( + `Using Google Analytics in DEBUG mode. Emulators (+ UI) events will be shown in GA Debug View only.`, + ); + return true; + } catch { + // The file above present in the repo but not packaged to npm. If require + // fails, just turn off debug mode since the CLI is not in development. + } + } + return false; +} + +// The Tracking ID for the Universal Analytics property for all of the CLI +// including emulator-related commands (double-tracked for historical reasons) +// but excluding Emulator UI. +// TODO: Upgrade to GA4 before July 1, 2023. See: +// https://support.google.com/analytics/answer/11583528 +const FIREBASE_ANALYTICS_UA = process.env.FIREBASE_ANALYTICS_UA || "UA-29174744-3"; + +let visitor: ua.Visitor; + +function ensureUAVisitor(): void { + if (!visitor) { + // Identifier for the client (UUID) in the CLI UA. + let anonId = configstore.get("analytics-uuid") as string; + if (!anonId) { + anonId = uuidV4(); + configstore.set("analytics-uuid", anonId); + } + + visitor = ua(FIREBASE_ANALYTICS_UA, anonId, { + strictCidFormat: false, + https: true, + }); + + visitor.set("cd1", process.platform); // Platform + visitor.set("cd2", process.version); // NodeVersion + visitor.set("cd3", process.env.FIREPIT_VERSION || "none"); // FirepitVersion + } +} + +export function track(action: string, label: string, duration = 0): Promise { + ensureUAVisitor(); + return new Promise((resolve) => { + if (usageEnabled() && configstore.get("tokens")) { + visitor.event("Firebase CLI " + pkg.version, action, label, duration).send(() => { + // we could handle errors here, but we won't + resolve(); + }); + } else { + resolve(); + } + }); +} diff --git a/src/types/auth/index.d.ts b/src/types/auth/index.d.ts new file mode 100644 index 00000000000..24d1c9d4ef9 --- /dev/null +++ b/src/types/auth/index.d.ts @@ -0,0 +1,54 @@ +// The wire protocol for an access token returned by Google. +// When we actually refresh from the server we should always have +// these optional fields, but when a user passes --token we may +// only have access_token. +export interface Tokens { + id_token?: string; + access_token: string; + refresh_token?: string; + scopes?: string[]; +} + +export interface User { + email: string; + + iss?: string; + azp?: string; + aud?: string; + sub?: number; + hd?: string; + email_verified?: boolean; + at_hash?: string; + iat?: number; + exp?: number; +} + +export interface Account { + user: User; + tokens: Tokens; +} +export interface TokensWithExpiration extends Tokens { + expires_at?: number; +} +export interface TokensWithTTL extends Tokens { + expires_in?: number; +} + +export interface AuthError { + error?: string; + error_description?: string; + error_uri?: string; + error_subtype?: string; +} + +export interface UserCredentials { + user: string | User; + tokens: TokensWithExpiration; + scopes: string[]; +} +// https://docs.github.com/en/developers/apps/authorizing-oauth-apps +export interface GitHubAuthResponse { + access_token: string; + scope: string; + token_type: string; +} diff --git a/src/types/extractTriggers.d.ts b/src/types/extractTriggers.d.ts index 7c021c382de..3032a71f2f3 100644 --- a/src/types/extractTriggers.d.ts +++ b/src/types/extractTriggers.d.ts @@ -9,5 +9,5 @@ import { ParsedTriggerDefinition } from "../emulator/functionsEmulatorShared"; export declare function extractTriggers( mod: Array, // eslint-disable-line @typescript-eslint/ban-types triggers: ParsedTriggerDefinition[], - prefix?: string + prefix?: string, ): void; diff --git a/src/types/marked-terminal/index.d.ts b/src/types/marked-terminal/index.d.ts deleted file mode 100644 index bf4fea68ae8..00000000000 --- a/src/types/marked-terminal/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module "marked-terminal" { - import * as marked from "marked"; - - class TerminalRenderer extends marked.Renderer { - constructor(options?: marked.MarkedOptions); - } - - export = TerminalRenderer; -} diff --git a/src/types/project/index.d.ts b/src/types/project/index.d.ts new file mode 100644 index 00000000000..57241aefe20 --- /dev/null +++ b/src/types/project/index.d.ts @@ -0,0 +1,25 @@ +export interface CloudProjectInfo { + project: string /* The resource name of the GCP project: "projects/projectId" */; + displayName?: string; + locationId?: string; +} + +export interface ProjectPage { + projects: T[]; + nextPageToken?: string; +} + +export interface FirebaseProjectMetadata { + name: string /* The fully qualified resource name of the Firebase project */; + projectId: string; + projectNumber: string; + displayName: string; + resources?: DefaultProjectResources; +} + +export interface DefaultProjectResources { + hostingSite?: string; + realtimeDatabaseInstance?: string; + storageBucket?: string; + locationId?: string; +} diff --git a/src/types/update-notifier-cjs.d.ts b/src/types/update-notifier-cjs.d.ts new file mode 100644 index 00000000000..4f083b15f6b --- /dev/null +++ b/src/types/update-notifier-cjs.d.ts @@ -0,0 +1,4 @@ +declare module "update-notifier-cjs" { + import m from "update-notifier"; + export = m; +} diff --git a/src/unzip.spec.ts b/src/unzip.spec.ts new file mode 100644 index 00000000000..ed7a197e7b4 --- /dev/null +++ b/src/unzip.spec.ts @@ -0,0 +1,43 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import { tmpdir } from "os"; +import * as path from "path"; +import { unzip } from "./unzip"; +import { ZIP_CASES } from "./test/fixtures/zip-files"; + +describe("unzip", () => { + let tempDir: string; + + before(async () => { + tempDir = await fs.promises.mkdtemp(path.join(tmpdir(), "firebasetest-")); + }); + + after(async () => { + await fs.promises.rmdir(tempDir, { recursive: true }); + }); + + for (const { name, archivePath, inflatedDir } of ZIP_CASES) { + it(`should unzip a zip file with ${name} case`, async () => { + const unzipPath = path.join(tempDir, name); + await unzip(archivePath, unzipPath); + + const expectedSize = await calculateFolderSize(inflatedDir); + expect(await calculateFolderSize(unzipPath)).to.eql(expectedSize); + }); + } +}); + +async function calculateFolderSize(folderPath: string): Promise { + const files = await fs.promises.readdir(folderPath); + let size = 0; + for (const file of files) { + const filePath = path.join(folderPath, file); + const stat = await fs.promises.stat(filePath); + if (stat.isDirectory()) { + size += await calculateFolderSize(filePath); + } else { + size += stat.size; + } + } + return size; +} diff --git a/src/unzip.ts b/src/unzip.ts new file mode 100644 index 00000000000..dc18616d8ae --- /dev/null +++ b/src/unzip.ts @@ -0,0 +1,164 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as zlib from "zlib"; +import { Readable, Transform, TransformCallback } from "stream"; +import { promisify } from "util"; +import { FirebaseError } from "./error"; +import { pipeline } from "stream"; +import { logger } from "./logger"; + +const pipelineAsync = promisify(pipeline); + +interface ZipEntry { + generalPurposeBitFlag: number; + compressedSize: number; + uncompressedSize: number; + fileNameLength: number; + extraLength: number; + fileName: string; + headerSize: number; + compressedData: Buffer; +} + +const readUInt32LE = (buf: Buffer, offset: number): number => { + return ( + (buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16) | (buf[offset + 3] << 24)) >>> 0 + ); +}; + +const findNextDataDescriptor = (data: Buffer, offset: number): [number, number] => { + const dataDescriptorSignature = 0x08074b50; + let position = offset; + while (position < data.length) { + const potentialDescriptor = data.slice(position, position + 16); + if (readUInt32LE(potentialDescriptor, 0) === dataDescriptorSignature) { + logger.debug(`[unzip] found data descriptor signature @ ${position}`); + const compressedSize = readUInt32LE(potentialDescriptor, 8); + const uncompressedSize = readUInt32LE(potentialDescriptor, 12); + return [compressedSize, uncompressedSize]; + } + position++; + } + throw new FirebaseError( + "Unable to find compressed and uncompressed size of file in ZIP archive.", + ); +}; + +const extractEntriesFromBuffer = async (data: Buffer, outputDir: string): Promise => { + let position = 0; + logger.debug(`Data is ${data.length}`); + while (position < data.length) { + const entryHeader = data.slice(position, position + 30); + const entry: ZipEntry = {} as ZipEntry; + if (readUInt32LE(entryHeader, 0) !== 0x04034b50) { + break; + } + entry.generalPurposeBitFlag = entryHeader.readUint16LE(6); + entry.compressedSize = readUInt32LE(entryHeader, 18); + entry.uncompressedSize = readUInt32LE(entryHeader, 22); + entry.fileNameLength = entryHeader.readUInt16LE(26); + entry.extraLength = entryHeader.readUInt16LE(28); + entry.fileName = data.toString("utf-8", position + 30, position + 30 + entry.fileNameLength); + entry.headerSize = 30 + entry.fileNameLength + entry.extraLength; + let dataDescriptorSize = 0; + if ( + entry.generalPurposeBitFlag === 8 && + entry.compressedSize === 0 && + entry.uncompressedSize === 0 + ) { + // If set, entry header won't have compressed or uncompressed size set. + // Need to look ahead to data descriptor to find them. + const [compressedSize, uncompressedSize] = findNextDataDescriptor(data, position); + entry.compressedSize = compressedSize; + entry.uncompressedSize = uncompressedSize; + // If we hit this, we also need to skip over the data descriptor to read the next file + dataDescriptorSize = 16; + } + entry.compressedData = data.slice( + position + entry.headerSize, + position + entry.headerSize + entry.compressedSize, + ); + logger.debug( + `[unzip] Entry: ${entry.fileName} (compressed_size=${entry.compressedSize} bytes, uncompressed_size=${entry.uncompressedSize} bytes)`, + ); + + entry.fileName = entry.fileName.replace(/\//g, path.sep); + + const outputFilePath = path.normalize(path.join(outputDir, entry.fileName)); + + logger.debug(`[unzip] Processing entry: ${entry.fileName}`); + if (entry.fileName.endsWith(path.sep)) { + logger.debug(`[unzip] mkdir: ${outputFilePath}`); + await fs.promises.mkdir(outputFilePath, { recursive: true }); + } else { + const parentDir = outputFilePath.substring(0, outputFilePath.lastIndexOf(path.sep)); + logger.debug(`[unzip] else mkdir: ${parentDir}`); + await fs.promises.mkdir(parentDir, { recursive: true }); + + const compressionMethod = entryHeader.readUInt16LE(8); + if (compressionMethod === 0) { + // Store (no compression) + logger.debug(`[unzip] Writing file: ${outputFilePath}`); + await fs.promises.writeFile(outputFilePath, entry.compressedData); + } else if (compressionMethod === 8) { + // Deflate + logger.debug(`[unzip] deflating: ${outputFilePath}`); + await pipelineAsync( + Readable.from(entry.compressedData), + zlib.createInflateRaw(), + fs.createWriteStream(outputFilePath), + ); + } else { + throw new FirebaseError(`Unsupported compression method: ${compressionMethod}`); + } + } + + position += entry.headerSize + entry.compressedSize + dataDescriptorSize; + } +}; + +export const unzip = async (inputPath: string, outputDir: string): Promise => { + const data = await fs.promises.readFile(inputPath); + await extractEntriesFromBuffer(data, outputDir); +}; + +class UnzipTransform extends Transform { + private chunks: Buffer[] = []; + private _resolve?: () => unknown; + private _reject?: (e: Error) => unknown; + + constructor(private outputDir: string) { + super(); + } + + _transform(chunk: Buffer, _: unknown, callback: TransformCallback): void { + this.chunks.push(chunk); + callback(); + } + + async _flush(callback: TransformCallback): Promise { + try { + await extractEntriesFromBuffer(Buffer.concat(this.chunks), this.outputDir); + callback(); + this._resolve?.(); + } catch (error) { + const firebaseError = new FirebaseError("Unable to unzip the target", { + children: [error], + original: error instanceof Error ? error : undefined, + }); + callback(firebaseError); + this._reject?.(firebaseError); + } + } + + async promise(): Promise { + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } +} + +export const createUnzipTransform = (outputDir: string): UnzipTransform => { + return new UnzipTransform(outputDir); +}; diff --git a/src/utils.spec.ts b/src/utils.spec.ts new file mode 100644 index 00000000000..08262be62fb --- /dev/null +++ b/src/utils.spec.ts @@ -0,0 +1,493 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as utils from "./utils"; + +describe("utils", () => { + describe("consoleUrl", () => { + it("should create a console URL", () => { + expect(utils.consoleUrl("projectId", "/foo/bar")).to.equal( + "https://console.firebase.google.com/project/projectId/foo/bar", + ); + }); + }); + + describe("getInheritedOption", () => { + it("should chain up looking for a key", () => { + const o1 = {}; + const o2 = { parent: o1, foo: "bar" }; + const o3 = { parent: o2, bar: "foo" }; + const o4 = { parent: o3, baz: "zip" }; + + expect(utils.getInheritedOption(o4, "foo")).to.equal("bar"); + }); + + it("should return undefined if the key does not exist", () => { + const o1 = {}; + const o2 = { parent: o1, foo: "bar" }; + const o3 = { parent: o2, bar: "foo" }; + const o4 = { parent: o3, baz: "zip" }; + + expect(utils.getInheritedOption(o4, "zip")).to.equal(undefined); + }); + }); + + describe("envOverride", () => { + it("should return the value if no current value exists", () => { + expect(utils.envOverride("FOOBARBAZ", "notset")).to.equal("notset"); + }); + + it("should set an override if it conflicts", () => { + process.env.FOO_BAR_BAZ = "set"; + + expect(utils.envOverride("FOO_BAR_BAZ", "notset")).to.equal("set"); + expect(utils.envOverrides).to.contain("FOO_BAR_BAZ"); + + delete process.env.FOO_BAR_BAZ; + }); + + it("should coerce the value", () => { + process.env.FOO_BAR_BAZ = "set"; + + expect(utils.envOverride("FOO_BAR_BAZ", "notset", (s) => s.split(""))).to.deep.equal([ + "s", + "e", + "t", + ]); + + delete process.env.FOO_BAR_BAZ; + }); + + it("should return provided value if coerce fails", () => { + process.env.FOO_BAR_BAZ = "set"; + + const coerce = () => { + throw new Error(); + }; + expect(utils.envOverride("FOO_BAR_BAZ", "notset", coerce)).to.deep.equal("notset"); + + delete process.env.FOO_BAR_BAZ; + }); + }); + + describe("isCloudEnvironment", () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("should return false by default", () => { + expect(utils.isCloudEnvironment()).to.be.false; + }); + + it("should return true when in codespaces", () => { + process.env.CODESPACES = "true"; + expect(utils.isCloudEnvironment()).to.be.true; + }); + + it("should return true when in Cloud Workstations", () => { + process.env.GOOGLE_CLOUD_WORKSTATIONS = "true"; + expect(utils.isCloudEnvironment()).to.be.true; + }); + }); + + describe("getDatabaseUrl", () => { + it("should create a url for prod", () => { + expect(utils.getDatabaseUrl("https://firebaseio.com", "fir-proj", "/")).to.equal( + "https://fir-proj.firebaseio.com/", + ); + expect(utils.getDatabaseUrl("https://firebaseio.com", "fir-proj", "/foo/bar")).to.equal( + "https://fir-proj.firebaseio.com/foo/bar", + ); + expect(utils.getDatabaseUrl("https://firebaseio.com", "fir-proj", "/foo/bar.json")).to.equal( + "https://fir-proj.firebaseio.com/foo/bar.json", + ); + expect( + utils.getDatabaseUrl( + "https://some-namespace.europe-west1.firebasedatabase.app", + "some-namespace", + "/foo/bar.json", + ), + ).to.equal("https://some-namespace.europe-west1.firebasedatabase.app/foo/bar.json"); + expect( + utils.getDatabaseUrl( + "https://europe-west1.firebasedatabase.app", + "some-namespace", + "/foo/bar.json", + ), + ).to.equal("https://some-namespace.europe-west1.firebasedatabase.app/foo/bar.json"); + }); + + it("should create a url for the emulator", () => { + expect(utils.getDatabaseUrl("http://localhost:9000", "fir-proj", "/")).to.equal( + "http://localhost:9000/?ns=fir-proj", + ); + expect(utils.getDatabaseUrl("http://localhost:9000", "fir-proj", "/foo/bar")).to.equal( + "http://localhost:9000/foo/bar?ns=fir-proj", + ); + expect(utils.getDatabaseUrl("http://localhost:9000", "fir-proj", "/foo/bar.json")).to.equal( + "http://localhost:9000/foo/bar.json?ns=fir-proj", + ); + }); + }); + + describe("getDatabaseViewDataUrl", () => { + it("should get a view data url for legacy prod URL", () => { + expect( + utils.getDatabaseViewDataUrl("https://firebaseio.com", "fir-proj", "fir-ns", "/foo/bar"), + ).to.equal( + "https://console.firebase.google.com/project/fir-proj/database/fir-ns/data/foo/bar", + ); + }); + + it("should get a view data url for new prod URL", () => { + expect( + utils.getDatabaseViewDataUrl( + "https://firebasedatabase.app", + "fir-proj", + "fir-ns", + "/foo/bar", + ), + ).to.equal( + "https://console.firebase.google.com/project/fir-proj/database/fir-ns/data/foo/bar", + ); + }); + + it("should get a view data url for the emulator", () => { + expect( + utils.getDatabaseViewDataUrl("http://localhost:9000", "fir-proj", "fir-ns", "/foo/bar"), + ).to.equal("http://localhost:9000/foo/bar.json?ns=fir-ns"); + }); + }); + + describe("addDatabaseNamespace", () => { + it("should add the namespace for prod", () => { + expect(utils.addDatabaseNamespace("https://firebaseio.com/", "fir-proj")).to.equal( + "https://fir-proj.firebaseio.com/", + ); + expect(utils.addDatabaseNamespace("https://firebaseio.com/foo/bar", "fir-proj")).to.equal( + "https://fir-proj.firebaseio.com/foo/bar", + ); + }); + + it("should add the namespace for the emulator", () => { + expect(utils.addDatabaseNamespace("http://localhost:9000/", "fir-proj")).to.equal( + "http://localhost:9000/?ns=fir-proj", + ); + expect(utils.addDatabaseNamespace("http://localhost:9000/foo/bar", "fir-proj")).to.equal( + "http://localhost:9000/foo/bar?ns=fir-proj", + ); + }); + }); + + describe("addSubdomain", () => { + it("should add a subdomain", () => { + expect(utils.addSubdomain("https://example.com", "sub")).to.equal("https://sub.example.com"); + }); + }); + + describe("endpoint", () => { + it("should join our strings", () => { + expect(utils.endpoint(["foo", "bar"])).to.equal("/foo/bar"); + }); + }); + + describe("promiseAllSettled", () => { + it("should settle all promises", async () => { + const result = await utils.promiseAllSettled([ + Promise.resolve("foo"), + Promise.reject("bar"), + Promise.resolve("baz"), + ]); + expect(result).to.deep.equal([ + { state: "fulfilled", value: "foo" }, + { state: "rejected", reason: "bar" }, + { state: "fulfilled", value: "baz" }, + ]); + }); + }); + + describe("promiseProps", () => { + it("should resolve all promises", async () => { + const o = { + foo: new Promise((resolve) => { + setTimeout(() => { + resolve("1"); + }); + }), + bar: Promise.resolve("2"), + }; + + const result = await utils.promiseProps(o); + expect(result).to.deep.equal({ + foo: "1", + bar: "2", + }); + }); + + it("should pass through objects", async () => { + const o = { + foo: new Promise((resolve) => { + setTimeout(() => { + resolve("1"); + }); + }), + bar: ["bar"], + }; + + const result = await utils.promiseProps(o); + expect(result).to.deep.equal({ + foo: "1", + bar: ["bar"], + }); + }); + + it("should reject if a promise rejects", async () => { + const o = { + foo: new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("1")); + }); + }), + bar: Promise.resolve("2"), + }; + + return expect(utils.promiseProps(o)).to.eventually.be.rejected; + }); + }); + + describe("datetimeString", () => { + it("should output the date in the correct format", () => { + // Don't worry about the hour since timezones screw everything up. + expect(utils.datetimeString(new Date("February 22, 2020 11:35:45-07:00"))).to.match( + /^2020-02-22 \d\d:35:45$/, + ); + expect(utils.datetimeString(new Date("February 7, 2020 11:35:45-07:00"))).to.match( + /^2020-02-07 \d\d:35:45$/, + ); + expect(utils.datetimeString(new Date("February 7, 2020 8:01:01-07:00"))).to.match( + /^2020-02-07 \d\d:01:01$/, + ); + }); + }); + + describe("streamToString/stringToStream", () => { + it("should be able to create and read streams", async () => { + const stream = utils.stringToStream("hello world"); + if (!stream) { + throw new Error("stream came back undefined"); + } + await expect(utils.streamToString(stream)).to.eventually.equal("hello world"); + }); + }); + + describe("allSettled", () => { + it("handles arrays of length zero", async () => { + const res = await utils.allSettled([]); + expect(res).to.deep.equal([]); + }); + + it("handles a simple success", async () => { + const res = await utils.allSettled([Promise.resolve(42)]); + expect(res).to.deep.equal([ + { + status: "fulfilled", + value: 42, + }, + ]); + }); + + it("handles a simple failure", async () => { + const res = await utils.allSettled([Promise.reject(new Error("oh noes!"))]); + expect(res.length).to.equal(1); + expect(res[0].status).to.equal("rejected"); + expect((res[0] as utils.PromiseRejectedResult).reason).to.be.instanceOf(Error); + expect((res[0] as any).reason?.message).to.equal("oh noes!"); + }); + + it("waits for all settled", async () => { + // Intetionally failing with a non-error to make matching easier + const reject = Promise.reject("fail fast"); + const resolve = new Promise((res) => { + setTimeout(() => res(42), 20); + }); + + const results = await utils.allSettled([reject, resolve]); + expect(results).to.deep.equal([ + { status: "rejected", reason: "fail fast" }, + { status: "fulfilled", value: 42 }, + ]); + }); + }); + + describe("groupBy", () => { + it("should transform simple array by fn", () => { + const arr = [3.4, 1.2, 7.7, 2, 3.9]; + expect(utils.groupBy(arr, Math.floor)).to.deep.equal({ + 1: [1.2], + 2: [2], + 3: [3.4, 3.9], + 7: [7.7], + }); + }); + + it("should transform array of objects by fn", () => { + const arr = [ + { id: 1, location: "us" }, + { id: 2, location: "us" }, + { id: 3, location: "asia" }, + { id: 4, location: "europe" }, + { id: 5, location: "asia" }, + ]; + expect(utils.groupBy(arr, (obj) => obj.location)).to.deep.equal({ + us: [ + { id: 1, location: "us" }, + { id: 2, location: "us" }, + ], + asia: [ + { id: 3, location: "asia" }, + { id: 5, location: "asia" }, + ], + europe: [{ id: 4, location: "europe" }], + }); + }); + }); + + describe("withTimeout", () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + clock.reset(); + }); + + afterEach(() => { + clock.restore(); + }); + + it("should fulfill if the original promise fulfills within timeout", async () => { + const promise = new Promise((resolve) => { + setTimeout(() => resolve("foo"), 1000); + }); + const wrapped = utils.withTimeout(5000, promise); + + clock.tick(1001); + await expect(wrapped).to.eventually.equal("foo"); + }); + + it("should reject if the original promise rejects within timeout", async () => { + const promise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("oh snap")), 1000); + }); + const wrapped = utils.withTimeout(5000, promise); + + clock.tick(1001); + await expect(wrapped).to.be.rejectedWith("oh snap"); + }); + + it("should reject with timeout if the original promise takes too long to fulfill", async () => { + const promise = new Promise((resolve) => { + setTimeout(() => resolve(42), 1000); + }); + const wrapped = utils.withTimeout(5000, promise); + + clock.tick(5001); + await expect(wrapped).to.be.rejectedWith("Timed out."); + }); + + it("should reject with timeout if the original promise takes too long to reject", async () => { + const promise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("oh snap")), 10000); + }); + const wrapped = utils.withTimeout(5000, promise); + + clock.tick(5001); + await expect(wrapped).to.be.rejectedWith("Timed out."); + }); + }); + + describe("debounce", () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it("should be called only once in the given time interval", () => { + const fn = sinon.stub(); + const debounced = utils.debounce(fn, 1000); + + for (let i = 0; i < 100; i++) { + debounced(i); + } + + clock.tick(1001); + expect(fn).to.be.calledOnce; + expect(fn).to.be.calledOnceWith(99); + }); + + it("should be called only once if it is called many times within the interval", () => { + const fn = sinon.stub(); + const debounced = utils.debounce(fn, 1000); + + for (let i = 0; i < 100; i++) { + debounced(i); + clock.tick(999); + } + + clock.tick(1001); + expect(fn).to.be.calledOnce; + expect(fn).to.be.calledOnceWith(99); + }); + + it("should be called only once within the interval if leading is provided", () => { + const fn = sinon.stub(); + const debounced = utils.debounce(fn, 1000, { leading: true }); + + for (let i = 0; i < 100; i++) { + debounced(i); + } + + clock.tick(999); + expect(fn).to.be.calledOnce; + expect(fn).to.be.calledOnceWith(0); + }); + + it("should be called twice with leading", () => { + const fn = sinon.stub(); + const debounced = utils.debounce(fn, 1000, { leading: true }); + + for (let i = 0; i < 100; i++) { + debounced(i); + } + + clock.tick(1500); + expect(fn).to.be.calledTwice; + expect(fn).to.be.calledWith(0); + expect(fn).to.be.calledWith(99); + }); + }); + + describe("connnectableHostname", () => { + it("should change wildcard IP addresses to corresponding loopbacks", () => { + expect(utils.connectableHostname("0.0.0.0")).to.equal("127.0.0.1"); + expect(utils.connectableHostname("::")).to.equal("::1"); + expect(utils.connectableHostname("[::]")).to.equal("[::1]"); + }); + it("should not change non-wildcard IP addresses or hostnames", () => { + expect(utils.connectableHostname("169.254.20.1")).to.equal("169.254.20.1"); + expect(utils.connectableHostname("fe80::1")).to.equal("fe80::1"); + expect(utils.connectableHostname("[fe80::2]")).to.equal("[fe80::2]"); + expect(utils.connectableHostname("example.com")).to.equal("example.com"); + }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 033ebad392e..6c177b87ecb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,34 +1,44 @@ +import * as fs from "fs-extra"; +import * as tty from "tty"; +import * as path from "node:path"; +import * as yaml from "yaml"; +import { Socket } from "node:net"; + import * as _ from "lodash"; import * as url from "url"; import * as http from "http"; -import * as clc from "cli-color"; +import * as clc from "colorette"; +import * as open from "open"; import * as ora from "ora"; import * as process from "process"; import { Readable } from "stream"; import * as winston from "winston"; import { SPLAT } from "triple-beam"; import { AssertionError } from "assert"; -const ansiStrip = require("cli-color/strip") as (input: string) => string; +const stripAnsi = require("strip-ansi"); +import { getPortPromise as getPort } from "portfinder"; import { configstore } from "./configstore"; import { FirebaseError } from "./error"; import { logger, LogLevel } from "./logger"; import { LogDataOrUndefined } from "./emulator/loggingEmulator"; -import { Socket } from "net"; +import { promptOnce } from "./prompt"; +import { readTemplateSync } from "./templates"; -const IS_WINDOWS = process.platform === "win32"; +export const IS_WINDOWS = process.platform === "win32"; const SUCCESS_CHAR = IS_WINDOWS ? "+" : "✔"; const WARNING_CHAR = IS_WINDOWS ? "!" : "⚠"; +const ERROR_CHAR = IS_WINDOWS ? "!!" : "⬢"; const THIRTY_DAYS_IN_MILLISECONDS = 30 * 24 * 60 * 60 * 1000; export const envOverrides: string[] = []; - +export const vscodeEnvVars: { [key: string]: string } = {}; /** * Create a Firebase Console URL for the specified path and project. */ export function consoleUrl(project: string, path: string): string { const api = require("./api"); - return `${api.consoleOrigin}/project/${project}${path}`; + return `${api.consoleOrigin()}/project/${project}${path}`; } /** @@ -38,13 +48,22 @@ export function consoleUrl(project: string, path: string): string { export function getInheritedOption(options: any, key: string): any { let target = options; while (target) { - if (_.has(target, key)) { + if (target[key] !== undefined) { return target[key]; } target = target.parent; } } +/** + * Sets the VSCode environment variables to be used by the CLI when called by VSCode + * @param envVar name of the environment variable + * @param value value of the environment variable + */ +export function setVSCodeEnvVars(envVar: string, value: string) { + vscodeEnvVars[envVar] = value; +} + /** * Override a value with supplied environment variable if present. A function * that returns the environment variable in an acceptable format can be @@ -53,15 +72,16 @@ export function getInheritedOption(options: any, key: string): any { export function envOverride( envname: string, value: string, - coerce?: (value: string, defaultValue: string) => any + coerce?: (value: string, defaultValue: string) => any, ): string { - const currentEnvValue = process.env[envname]; + const currentEnvValue = + isVSCodeExtension() && vscodeEnvVars[envname] ? vscodeEnvVars[envname] : process.env[envname]; if (currentEnvValue && currentEnvValue.length) { envOverrides.push(envname); if (coerce) { try { return coerce(currentEnvValue, value); - } catch (e) { + } catch (e: any) { return value; } } @@ -87,7 +107,7 @@ export function getDatabaseViewDataUrl( origin: string, project: string, namespace: string, - pathname: string + pathname: string, ): string { const urlObj = new url.URL(origin); if (urlObj.hostname.includes("firebaseio") || urlObj.hostname.includes("firebasedatabase")) { @@ -128,9 +148,9 @@ export function addSubdomain(origin: string, subdomain: string): string { export function logSuccess( message: string, type: LogLevel = "info", - data: LogDataOrUndefined = undefined + data: LogDataOrUndefined = undefined, ): void { - logger[type](clc.green.bold(`${SUCCESS_CHAR} `), message, data); + logger[type](clc.green(clc.bold(`${SUCCESS_CHAR} `)), message, data); } /** @@ -140,9 +160,9 @@ export function logLabeledSuccess( label: string, message: string, type: LogLevel = "info", - data: LogDataOrUndefined = undefined + data: LogDataOrUndefined = undefined, ): void { - logger[type](clc.green.bold(`${SUCCESS_CHAR} ${label}:`), message, data); + logger[type](clc.green(clc.bold(`${SUCCESS_CHAR} ${label}:`)), message, data); } /** @@ -151,9 +171,9 @@ export function logLabeledSuccess( export function logBullet( message: string, type: LogLevel = "info", - data: LogDataOrUndefined = undefined + data: LogDataOrUndefined = undefined, ): void { - logger[type](clc.cyan.bold("i "), message, data); + logger[type](clc.cyan(clc.bold("i ")), message, data); } /** @@ -163,9 +183,9 @@ export function logLabeledBullet( label: string, message: string, type: LogLevel = "info", - data: LogDataOrUndefined = undefined + data: LogDataOrUndefined = undefined, ): void { - logger[type](clc.cyan.bold(`i ${label}:`), message, data); + logger[type](clc.cyan(clc.bold(`i ${label}:`)), message, data); } /** @@ -174,9 +194,9 @@ export function logLabeledBullet( export function logWarning( message: string, type: LogLevel = "warn", - data: LogDataOrUndefined = undefined + data: LogDataOrUndefined = undefined, ): void { - logger[type](clc.yellow.bold(`${WARNING_CHAR} `), message, data); + logger[type](clc.yellow(clc.bold(`${WARNING_CHAR} `)), message, data); } /** @@ -186,9 +206,21 @@ export function logLabeledWarning( label: string, message: string, type: LogLevel = "warn", - data: LogDataOrUndefined = undefined + data: LogDataOrUndefined = undefined, ): void { - logger[type](clc.yellow.bold(`${WARNING_CHAR} ${label}:`), message, data); + logger[type](clc.yellow(clc.bold(`${WARNING_CHAR} ${label}:`)), message, data); +} + +/** + * Log an error statement with a red bullet at the start of the line. + */ +export function logLabeledError( + label: string, + message: string, + type: LogLevel = "error", + data: LogDataOrUndefined = undefined, +): void { + logger[type](clc.red(clc.bold(`${ERROR_CHAR} ${label}:`)), message, data); } /** @@ -217,9 +249,7 @@ export type PromiseResult = PromiseFulfilledResult | PromiseRejectedResult * TODO: delete once min Node version is 12.9.0 or greater */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function allSettled( - promises: Array> -): Promise>> { +export function allSettled(promises: Array>): Promise>> { if (!promises.length) { return Promise.resolve([]); } @@ -244,7 +274,7 @@ export function allSettled( status: "rejected", reason: err, }; - } + }, ) .then(() => { if (!--remaining) { @@ -301,7 +331,7 @@ export function streamToString(s: NodeJS.ReadableStream): Promise { /** * Sets the active project alias or id in the specified directory. */ -export function makeActiveProject(projectDir: string, newActive: string | null): void { +export function makeActiveProject(projectDir: string, newActive?: string): void { const activeProjects = configstore.get("activeProjects") || {}; if (newActive) { activeProjects[projectDir] = newActive; @@ -315,7 +345,7 @@ export function makeActiveProject(projectDir: string, newActive: string | null): * Creates API endpoint string, e.g. /v1/projects/pid/cloudfunctions */ export function endpoint(parts: string[]): string { - return `/${_.join(parts, "/")}`; + return `/${parts.join("/")}`; } /** @@ -326,23 +356,23 @@ export function getFunctionsEventProvider(eventType: string): string { // Legacy event types: const parts = eventType.split("/"); if (parts.length > 1) { - const provider = _.last(parts[1].split(".")); + const provider = last(parts[1].split(".")); return _.capitalize(provider); } - // New event types: - if (eventType.match(/google.pubsub/)) { + // 1st gen event types: + if (/google.*pubsub/.exec(eventType)) { return "PubSub"; - } else if (eventType.match(/google.storage/)) { + } else if (/google.storage/.exec(eventType)) { return "Storage"; - } else if (eventType.match(/google.analytics/)) { + } else if (/google.analytics/.exec(eventType)) { return "Analytics"; - } else if (eventType.match(/google.firebase.database/)) { + } else if (/google.firebase.database/.exec(eventType)) { return "Database"; - } else if (eventType.match(/google.firebase.auth/)) { + } else if (/google.firebase.auth/.exec(eventType)) { return "Auth"; - } else if (eventType.match(/google.firebase.crashlytics/)) { + } else if (/google.firebase.crashlytics/.exec(eventType)) { return "Crashlytics"; - } else if (eventType.match(/google.firestore/)) { + } else if (/google.*firestore/.exec(eventType)) { return "Firestore"; } return _.capitalize(eventType.split(".")[1]); @@ -365,11 +395,11 @@ export type SettledPromise = SettledPromiseResolved | SettledPromiseRejected; * either resolved or rejected. */ export function promiseAllSettled(promises: Array>): Promise { - const wrappedPromises = _.map(promises, async (p) => { + const wrappedPromises = promises.map(async (p) => { try { const val = await Promise.resolve(p); return { state: "fulfilled", value: val } as SettledPromiseResolved; - } catch (err) { + } catch (err: any) { return { state: "rejected", reason: err } as SettledPromiseRejected; } }); @@ -383,7 +413,7 @@ export function promiseAllSettled(promises: Array>): Promise( action: () => Promise, check: (value: T) => boolean, - interval = 2500 + interval = 2500, ): Promise { return new Promise((resolve, promiseReject) => { const run = async () => { @@ -393,7 +423,7 @@ export async function promiseWhile( return resolve(res); } setTimeout(run, interval); - } catch (err) { + } catch (err: any) { return promiseReject(err); } }; @@ -401,13 +431,35 @@ export async function promiseWhile( }); } +/** + * Return a promise that rejects after timeoutMs but otherwise behave the same. + * @param timeoutMs the time in milliseconds before forced rejection + * @param promise the original promise + * @return a promise wrapping the original promise with rejection on timeout + */ +export function withTimeout(timeoutMs: number, promise: Promise): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("Timed out.")), timeoutMs); + promise.then( + (value) => { + clearTimeout(timeout); + resolve(value); + }, + (err) => { + clearTimeout(timeout); + reject(err); + }, + ); + }); +} + /** * Resolves all Promises at every key in the given object. If a value is not a * Promise, it is returned as-is. */ export async function promiseProps(obj: any): Promise { const resultObj: any = {}; - const promises = _.keys(obj).map(async (key) => { + const promises = Object.keys(obj).map(async (key) => { const r = await Promise.resolve(obj[key]); resultObj[key] = r; }); @@ -446,6 +498,9 @@ export function tryParse(value: any) { } } +/** + * + */ export function setupLoggers() { if (process.env.DEBUG) { logger.add( @@ -453,9 +508,9 @@ export function setupLoggers() { level: "debug", format: winston.format.printf((info) => { const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify); - return `${ansiStrip(segments.join(" "))}`; + return `${stripAnsi(segments.join(" "))}`; }), - }) + }), ); } else if (process.env.IS_FIREBASE_CLI) { logger.add( @@ -463,10 +518,10 @@ export function setupLoggers() { level: "info", format: winston.format.printf((info) => [info.message, ...(info[SPLAT] || [])] - .filter((chunk) => typeof chunk == "string") - .join(" ") + .filter((chunk) => typeof chunk === "string") + .join(" "), ), - }) + }), ); } } @@ -480,7 +535,7 @@ export async function promiseWithSpinner(action: () => Promise, message: s try { data = await action(); spinner.succeed(); - } catch (err) { + } catch (err: any) { spinner.fail(); throw err; } @@ -488,13 +543,17 @@ export async function promiseWithSpinner(action: () => Promise, message: s return data; } +/** Creates a promise that resolves after a given timeout. await to "sleep". */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + /** * Return a "destroy" function for a Node.js HTTP server. MUST be called on * server creation (e.g. right after `.listen`), BEFORE any connections. * * Inspired by https://github.com/isaacs/server-destroy/blob/master/index.js - * - * @returns a function that destroys all connections and closes the server + * @return a function that destroys all connections and closes the server */ export function createDestroyer(server: http.Server): () => Promise { const connections = new Set(); @@ -526,9 +585,10 @@ export function createDestroyer(server: http.Server): () => Promise { * @return the formatted date. */ export function datetimeString(d: Date): string { - const day = `${d.getFullYear()}-${(d.getMonth() + 1) + const day = `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d + .getDate() .toString() - .padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`; + .padStart(2, "0")}`; const time = `${d.getHours().toString().padStart(2, "0")}:${d .getMinutes() .toString() @@ -540,7 +600,14 @@ export function datetimeString(d: Date): string { * Indicates whether the end-user is running the CLI from a cloud-based environment. */ export function isCloudEnvironment() { - return !!process.env.CODESPACES; + return !!process.env.CODESPACES || !!process.env.GOOGLE_CLOUD_WORKSTATIONS; +} + +/** + * Detect if code is running in a VSCode Extension + */ +export function isVSCodeExtension(): boolean { + return !!process.env.VSCODE_CWD; } /** @@ -559,18 +626,9 @@ export function thirtyDaysFromNow(): Date { } /** - * See: - * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions + * Verifies val is a string. */ -export function assertDefined(val: T, message?: string): asserts val is NonNullable { - if (val === undefined || val === null) { - throw new AssertionError({ - message: message || `expected value to be defined but got "${val}"`, - }); - } -} - -export function assertIsString(val: any, message?: string): asserts val is string { +export function assertIsString(val: unknown, message?: string): asserts val is string { if (typeof val !== "string") { throw new AssertionError({ message: message || `expected "string" but got "${typeof val}"`, @@ -578,7 +636,10 @@ export function assertIsString(val: any, message?: string): asserts val is strin } } -export function assertIsNumber(val: any, message?: string): asserts val is number { +/** + * Verifies val is a number. + */ +export function assertIsNumber(val: unknown, message?: string): asserts val is number { if (typeof val !== "number") { throw new AssertionError({ message: message || `expected "number" but got "${typeof val}"`, @@ -586,9 +647,12 @@ export function assertIsNumber(val: any, message?: string): asserts val is numbe } } +/** + * Assert val is a string or undefined. + */ export function assertIsStringOrUndefined( - val: any, - message?: string + val: unknown, + message?: string, ): asserts val is string | undefined { if (!(val === undefined || typeof val === "string")) { throw new AssertionError({ @@ -596,3 +660,260 @@ export function assertIsStringOrUndefined( }); } } + +/** + * Polyfill for groupBy. + */ +export function groupBy( + arr: T[], + f: (item: T) => K, +): Record { + return arr.reduce( + (result, item) => { + const key = f(item); + if (result[key]) { + result[key].push(item); + } else { + result[key] = [item]; + } + return result; + }, + {} as Record, + ); +} + +function cloneArray(arr: T[]): T[] { + return arr.map((e) => cloneDeep(e)); +} + +function cloneObject>(obj: T): T { + const clone: Record = {}; + for (const [k, v] of Object.entries(obj)) { + clone[k] = cloneDeep(v); + } + return clone as T; +} + +/** + * replacement for lodash cloneDeep that preserves type. + */ +// TODO: replace with builtin once Node 18 becomes the min version. +export function cloneDeep(obj: T): T { + if (typeof obj !== "object" || !obj) { + return obj; + } + if (obj instanceof RegExp) { + return RegExp(obj, obj.flags) as typeof obj; + } + if (obj instanceof Date) { + return new Date(obj) as typeof obj; + } + if (Array.isArray(obj)) { + return cloneArray(obj) as typeof obj; + } + if (obj instanceof Map) { + return new Map(obj.entries()) as typeof obj; + } + return cloneObject(obj as Record) as typeof obj; +} + +/** + * Returns the last element in the array, or undefined if no array is passed or + * the array is empty. + */ +export function last(arr?: T[]): T { + // The type system should never allow this, so return something that violates + // the type system when passing in something that violates the type system. + if (!Array.isArray(arr)) { + return undefined as unknown as T; + } + return arr[arr.length - 1]; +} + +/** + * Options for debounce. + */ +type DebounceOptions = { + leading?: boolean; +}; + +/** + * Returns a function that delays invoking `fn` until `delay` ms have + * passed since the last time `fn` was invoked. + */ +export function debounce( + fn: (...args: T[]) => void, + delay: number, + { leading }: DebounceOptions = {}, +): (...args: T[]) => void { + let timer: NodeJS.Timeout; + return (...args) => { + if (!timer && leading) { + fn(...args); + } + clearTimeout(timer); + timer = setTimeout(() => fn(...args), delay); + }; +} + +/** + * Returns a random number between min and max, inclusive. + */ +export function randomInt(min: number, max: number): number { + min = Math.floor(min); + max = Math.ceil(max) + 1; + return Math.floor(Math.random() * (max - min) + min); +} + +/** + * Return a connectable hostname, replacing wildcard 0.0.0.0 or :: with loopback + * addresses 127.0.0.1 / ::1 correspondingly. See below for why this is needed: + * https://github.com/firebase/firebase-tools-ui/issues/286 + * + * This assumes that the consumer (i.e. client SDK, etc.) is located on the same + * device as the Emulator hub (i.e. CLI), which may not be true on multi-device + * setups, etc. In that case, the customer can work around this by specifying a + * non-wildcard IP address (like the IP address on LAN, if accessing via LAN). + */ +export function connectableHostname(hostname: string): string { + if (hostname === "0.0.0.0") { + hostname = "127.0.0.1"; + } else if (hostname === "::" /* unquoted IPv6 wildcard */) { + hostname = "::1"; + } else if (hostname === "[::]" /* quoted IPv6 wildcard */) { + hostname = "[::1]"; + } + return hostname; +} + +/** + * We wrap and export the open() function from the "open" package + * to stub it out in unit tests. + */ +export async function openInBrowser(url: string): Promise { + await open(url); +} + +/** + * Like openInBrowser but opens the url in a popup. + */ +export async function openInBrowserPopup( + url: string, + buttonText: string, +): Promise<{ url: string; cleanup: () => void }> { + const popupPage = readTemplateSync("popup.html") + .replace("${url}", url) + .replace("${buttonText}", buttonText); + + const port = await getPort(); + + const server = http.createServer((req, res) => { + res.writeHead(200, { + "Content-Length": popupPage.length, + "Content-Type": "text/html", + }); + res.end(popupPage); + req.socket.destroy(); + }); + + server.listen(port); + + const popupPageUri = `http://localhost:${port}`; + await openInBrowser(popupPageUri); + + return { + url: popupPageUri, + cleanup: () => { + server.close(); + }, + }; +} + +/** + * Get hostname from a given url or null if the url is invalid + */ +export function getHostnameFromUrl(url: string): string | null { + try { + return new URL(url).hostname; + } catch (e: unknown) { + return null; + } +} + +/** + * Retrieves a file from the directory. + */ +export function readFileFromDirectory( + directory: string, + file: string, +): Promise<{ source: string; sourceDirectory: string }> { + return new Promise((resolve, reject) => { + fs.readFile(path.resolve(directory, file), "utf8", (err, data) => { + if (err) { + if (err.code === "ENOENT") { + return reject( + new FirebaseError(`Could not find "${file}" in "${directory}"`, { original: err }), + ); + } + reject( + new FirebaseError(`Failed to read file "${file}" in "${directory}"`, { original: err }), + ); + } else { + resolve(data); + } + }); + }).then((source) => { + return { + source, + sourceDirectory: directory, + }; + }); +} + +/** + * Wrapps `yaml.safeLoad` with an error handler to present better YAML parsing + * errors. + */ +export function wrappedSafeLoad(source: string): any { + try { + return yaml.parse(source); + } catch (err: any) { + throw new FirebaseError(`YAML Error: ${err.message}`, { original: err }); + } +} + +/** + * Generate id meeting the following criterias: + * - Lowercase, digits, and hyphens only + * - Must begin with letter + * - Cannot end with hyphen + */ +export function generateId(n = 6): string { + const letters = "abcdefghijklmnopqrstuvwxyz"; + const allChars = "01234567890-abcdefghijklmnopqrstuvwxyz"; + let id = letters[Math.floor(Math.random() * letters.length)]; + for (let i = 1; i < n; i++) { + const idx = Math.floor(Math.random() * allChars.length); + id += allChars[idx]; + } + return id; +} + +/** + * Reads a secret value from either a file or a prompt. + * If dataFile is falsy and this is a tty, uses prompty. Otherwise reads from dataFile. + * If dataFile is - or falsy, this means reading from file descriptor 0 (e.g. pipe in) + */ +export function readSecretValue(prompt: string, dataFile?: string): Promise { + if ((!dataFile || dataFile === "-") && tty.isatty(0)) { + return promptOnce({ + type: "password", + message: prompt, + }); + } + let input: string | number = 0; + if (dataFile && dataFile !== "-") { + input = dataFile; + } + return Promise.resolve(fs.readFileSync(input, "utf-8")); +} diff --git a/standalone/firepit.js b/standalone/firepit.js index 137895d4ff7..f7927e1869e 100644 --- a/standalone/firepit.js +++ b/standalone/firepit.js @@ -184,7 +184,6 @@ let runtimeBinsPath = path.join(homePath, ".cache", "firebase", "runtime"); const npmArgs = [ `--script-shell=${runtimeBinsPath}/shell${isWindows ? ".bat" : ""}`, `--globalconfig=${path.join(runtimeBinsPath, "npmrc")}`, - `--userconfig=${path.join(runtimeBinsPath, "npmrc")}`, `--scripts-prepend-node-path=auto` ]; diff --git a/standalone/package-lock.json b/standalone/package-lock.json new file mode 100644 index 00000000000..d0f3dda33e9 --- /dev/null +++ b/standalone/package-lock.json @@ -0,0 +1,7019 @@ +{ + "name": "firepit", + "version": "1.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "firepit", + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "npm": "^8.19.0", + "shelljs": "^0.8.3", + "shx": "^0.3.2", + "user-home": "^2.0.0" + }, + "devDependencies": { + "pkg": "^5.7.0", + "prettier": "^1.15.3" + } + }, + "node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/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==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/multistream/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==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node_modules/node-abi": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", + "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npm": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/npm/-/npm-8.19.4.tgz", + "integrity": "sha512-3HANl8i9DKnUA89P4KEgVNN28EjSeDCmvEqbzOAuxCFDzdBZzjUl99zgnGpOUumvW5lvJo2HKcjrsc+tfyv1Hw==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/ci-detect", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/run-script", + "abbrev", + "archy", + "cacache", + "chalk", + "chownr", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "mkdirp", + "mkdirp-infer-owner", + "ms", + "node-gyp", + "nopt", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "opener", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "read-package-json", + "read-package-json-fast", + "readdir-scoped-modules", + "rimraf", + "semver", + "ssri", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^5.6.3", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/config": "^4.2.1", + "@npmcli/fs": "^2.1.0", + "@npmcli/map-workspaces": "^2.0.3", + "@npmcli/package-json": "^2.0.0", + "@npmcli/run-script": "^4.2.1", + "abbrev": "~1.1.1", + "archy": "~1.0.0", + "cacache": "^16.1.3", + "chalk": "^4.1.2", + "chownr": "^2.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.2", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.12", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "graceful-fs": "^4.2.10", + "hosted-git-info": "^5.2.1", + "ini": "^3.0.1", + "init-package-json": "^3.0.2", + "is-cidr": "^4.0.2", + "json-parse-even-better-errors": "^2.3.1", + "libnpmaccess": "^6.0.4", + "libnpmdiff": "^4.0.5", + "libnpmexec": "^4.0.14", + "libnpmfund": "^3.0.5", + "libnpmhook": "^8.0.4", + "libnpmorg": "^4.0.4", + "libnpmpack": "^4.1.3", + "libnpmpublish": "^6.0.5", + "libnpmsearch": "^5.0.4", + "libnpmteam": "^4.0.4", + "libnpmversion": "^3.0.7", + "make-fetch-happen": "^10.2.0", + "minimatch": "^5.1.0", + "minipass": "^3.1.6", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "ms": "^2.1.2", + "node-gyp": "^9.1.0", + "nopt": "^6.0.0", + "npm-audit-report": "^3.0.0", + "npm-install-checks": "^5.0.0", + "npm-package-arg": "^9.1.0", + "npm-pick-manifest": "^7.0.2", + "npm-profile": "^6.2.0", + "npm-registry-fetch": "^13.3.1", + "npm-user-validate": "^1.0.1", + "npmlog": "^6.0.2", + "opener": "^1.5.2", + "p-map": "^4.0.0", + "pacote": "^13.6.2", + "parse-conflict-json": "^2.0.2", + "proc-log": "^2.0.1", + "qrcode-terminal": "^0.12.0", + "read": "~1.0.7", + "read-package-json": "^5.0.2", + "read-package-json-fast": "^2.0.3", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^9.0.1", + "tar": "^6.1.11", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^2.0.0", + "validate-npm-package-name": "^4.0.0", + "which": "^2.0.2", + "write-file-atomic": "^4.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@colors/colors": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/@gar/promisify": { + "version": "1.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "5.6.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/map-workspaces": "^2.0.3", + "@npmcli/metavuln-calculator": "^3.0.1", + "@npmcli/move-file": "^2.0.0", + "@npmcli/name-from-folder": "^1.0.1", + "@npmcli/node-gyp": "^2.0.0", + "@npmcli/package-json": "^2.0.0", + "@npmcli/query": "^1.2.0", + "@npmcli/run-script": "^4.1.3", + "bin-links": "^3.0.3", + "cacache": "^16.1.3", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^5.2.1", + "json-parse-even-better-errors": "^2.3.1", + "json-stringify-nice": "^1.1.4", + "minimatch": "^5.1.0", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^6.0.0", + "npm-install-checks": "^5.0.0", + "npm-package-arg": "^9.0.0", + "npm-pick-manifest": "^7.0.2", + "npm-registry-fetch": "^13.0.0", + "npmlog": "^6.0.2", + "pacote": "^13.6.1", + "parse-conflict-json": "^2.0.1", + "proc-log": "^2.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.1", + "read-package-json-fast": "^2.0.2", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^9.0.0", + "treeverse": "^2.0.0", + "walk-up-path": "^1.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/ci-detect": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "4.2.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^2.0.2", + "ini": "^3.0.0", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^6.0.0", + "proc-log": "^2.0.0", + "read-package-json-fast": "^2.0.3", + "semver": "^7.3.5", + "walk-up-path": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "2.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^3.0.0", + "lru-cache": "^7.4.4", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^7.0.0", + "proc-log": "^2.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "1.0.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "installed-package-contents": "index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents/node_modules/npm-bundled": { + "version": "1.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "2.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^1.0.1", + "glob": "^8.0.1", + "minimatch": "^5.0.1", + "read-package-json-fast": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "3.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^16.0.0", + "json-parse-even-better-errors": "^2.3.1", + "pacote": "^13.0.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "infer-owner": "^1.0.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "1.2.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^9.1.0", + "postcss-selector-parser": "^6.0.10", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "4.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^2.0.0", + "@npmcli/promise-spawn": "^3.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^2.0.3", + "which": "^2.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@tootallnate/once": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "1.1.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/agent-base": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/npm/node_modules/agentkeepalive": { + "version": "4.2.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/asap": { + "version": "2.0.6", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^5.0.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0", + "read-cmd-shim": "^3.0.0", + "rimraf": "^3.0.0", + "write-file-atomic": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/bin-links/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "16.1.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "4.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "3.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^4.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mkdirp-infer-owner": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.6.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/debuglog": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/npm/node_modules/delegates": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/depd": { + "version": "1.1.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/dezalgo": { + "version": "1.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.1.0", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.12", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/fs.realpath": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/gauge": { + "version": "4.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "8.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.10", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/has": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/npm/node_modules/has-flag": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "5.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/humanize-ms": { + "version": "1.2.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^5.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/infer-owner": { + "version": "1.0.4", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/inflight": { + "version": "1.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/inherits": { + "version": "2.0.4", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/ini": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^9.0.1", + "promzard": "^0.3.0", + "read": "^1.0.7", + "read-package-json": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/ip": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "4.0.2", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^3.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.10.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "5.1.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.4.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "6.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "minipass": "^3.1.1", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "4.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/disparity-colors": "^2.0.0", + "@npmcli/installed-package-contents": "^1.0.7", + "binary-extensions": "^2.2.0", + "diff": "^5.1.0", + "minimatch": "^5.0.1", + "npm-package-arg": "^9.0.1", + "pacote": "^13.6.1", + "tar": "^6.1.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "4.0.14", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^5.6.3", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/fs": "^2.1.1", + "@npmcli/run-script": "^4.2.0", + "chalk": "^4.1.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-package-arg": "^9.0.1", + "npmlog": "^6.0.2", + "pacote": "^13.6.1", + "proc-log": "^2.0.0", + "read": "^1.0.7", + "read-package-json-fast": "^2.0.2", + "semver": "^7.3.7", + "walk-up-path": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "3.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^5.6.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "8.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "4.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "4.1.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/run-script": "^4.1.3", + "npm-package-arg": "^9.0.1", + "pacote": "^13.6.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "6.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "normalize-package-data": "^4.0.0", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0", + "semver": "^7.3.7", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "5.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "4.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "3.0.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^3.0.0", + "@npmcli/run-script": "^4.1.3", + "json-parse-even-better-errors": "^2.3.1", + "proc-log": "^2.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "7.13.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "10.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "5.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "3.3.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "1.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "2.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/mkdirp-infer-owner": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "infer-owner": "^1.0.4", + "mkdirp": "^1.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "0.0.8", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "9.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "4.0.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^5.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "5.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "9.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^5.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "validate-npm-package-name": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "5.1.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^8.0.1", + "ignore-walk": "^5.0.1", + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^5.0.0", + "npm-normalize-package-bin": "^2.0.0", + "npm-package-arg": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "6.2.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^13.0.1", + "proc-log": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "13.3.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^10.0.6", + "minipass": "^3.1.6", + "minipass-fetch": "^2.0.3", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^9.0.1", + "proc-log": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "1.0.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/npmlog": { + "version": "6.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/once": { + "version": "1.4.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/opener": { + "version": "1.5.2", + "inBundle": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "13.6.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^3.0.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/promise-spawn": "^3.0.0", + "@npmcli/run-script": "^4.1.0", + "cacache": "^16.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.6", + "mkdirp": "^1.0.4", + "npm-package-arg": "^9.0.0", + "npm-packlist": "^5.1.0", + "npm-pick-manifest": "^7.0.0", + "npm-registry-fetch": "^13.0.1", + "proc-log": "^2.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^5.0.0", + "read-package-json-fast": "^2.0.3", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.1", + "just-diff": "^5.0.1", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/path-is-absolute": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "0.3.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "1" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "1.0.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "5.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^8.0.1", + "json-parse-even-better-errors": "^2.3.1", + "normalize-package-data": "^4.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "2.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/read-package-json/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/readable-stream": { + "version": "3.6.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/rimraf": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.3.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "3.0.7", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.7.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.3.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.11", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "9.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/string_decoder": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "7.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.1.11", + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/npm/node_modules/wrappy": { + "version": "1.0.2", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "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==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/pkg-fetch/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pkg-fetch/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pkg-fetch/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pkg-fetch/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==", + "dev": true + }, + "node_modules/pkg-fetch/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-fetch/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pkg/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pkg/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pkg/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==", + "dev": true + }, + "node_modules/pkg/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve/node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.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==", + "dev": 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==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/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==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==", + "dependencies": { + "os-homedir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true + }, + "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==", + "dev": true + }, + "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==", + "dev": true, + "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==", + "dev": 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/wrap-ansi/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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true + }, + "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==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": 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==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + } + }, + "dependencies": { + "@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "requires": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-string-parser": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", + "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true + }, + "@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "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==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "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==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "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==", + "dev": true + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "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==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + }, + "into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "requires": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + } + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "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==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "requires": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node-abi": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", + "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "npm": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/npm/-/npm-8.19.4.tgz", + "integrity": "sha512-3HANl8i9DKnUA89P4KEgVNN28EjSeDCmvEqbzOAuxCFDzdBZzjUl99zgnGpOUumvW5lvJo2HKcjrsc+tfyv1Hw==", + "requires": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^5.6.3", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/config": "^4.2.1", + "@npmcli/fs": "^2.1.0", + "@npmcli/map-workspaces": "^2.0.3", + "@npmcli/package-json": "^2.0.0", + "@npmcli/run-script": "^4.2.1", + "abbrev": "~1.1.1", + "archy": "~1.0.0", + "cacache": "^16.1.3", + "chalk": "^4.1.2", + "chownr": "^2.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.2", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.12", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "graceful-fs": "^4.2.10", + "hosted-git-info": "^5.2.1", + "ini": "^3.0.1", + "init-package-json": "^3.0.2", + "is-cidr": "^4.0.2", + "json-parse-even-better-errors": "^2.3.1", + "libnpmaccess": "^6.0.4", + "libnpmdiff": "^4.0.5", + "libnpmexec": "^4.0.14", + "libnpmfund": "^3.0.5", + "libnpmhook": "^8.0.4", + "libnpmorg": "^4.0.4", + "libnpmpack": "^4.1.3", + "libnpmpublish": "^6.0.5", + "libnpmsearch": "^5.0.4", + "libnpmteam": "^4.0.4", + "libnpmversion": "^3.0.7", + "make-fetch-happen": "^10.2.0", + "minimatch": "^5.1.0", + "minipass": "^3.1.6", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "ms": "^2.1.2", + "node-gyp": "^9.1.0", + "nopt": "^6.0.0", + "npm-audit-report": "^3.0.0", + "npm-install-checks": "^5.0.0", + "npm-package-arg": "^9.1.0", + "npm-pick-manifest": "^7.0.2", + "npm-profile": "^6.2.0", + "npm-registry-fetch": "^13.3.1", + "npm-user-validate": "^1.0.1", + "npmlog": "^6.0.2", + "opener": "^1.5.2", + "p-map": "^4.0.0", + "pacote": "^13.6.2", + "parse-conflict-json": "^2.0.2", + "proc-log": "^2.0.1", + "qrcode-terminal": "^0.12.0", + "read": "~1.0.7", + "read-package-json": "^5.0.2", + "read-package-json-fast": "^2.0.3", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^9.0.1", + "tar": "^6.1.11", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^2.0.0", + "validate-npm-package-name": "^4.0.0", + "which": "^2.0.2", + "write-file-atomic": "^4.0.1" + }, + "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "bundled": true, + "optional": true + }, + "@gar/promisify": { + "version": "1.1.3", + "bundled": true + }, + "@isaacs/string-locale-compare": { + "version": "1.1.0", + "bundled": true + }, + "@npmcli/arborist": { + "version": "5.6.3", + "bundled": true, + "requires": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/map-workspaces": "^2.0.3", + "@npmcli/metavuln-calculator": "^3.0.1", + "@npmcli/move-file": "^2.0.0", + "@npmcli/name-from-folder": "^1.0.1", + "@npmcli/node-gyp": "^2.0.0", + "@npmcli/package-json": "^2.0.0", + "@npmcli/query": "^1.2.0", + "@npmcli/run-script": "^4.1.3", + "bin-links": "^3.0.3", + "cacache": "^16.1.3", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^5.2.1", + "json-parse-even-better-errors": "^2.3.1", + "json-stringify-nice": "^1.1.4", + "minimatch": "^5.1.0", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^6.0.0", + "npm-install-checks": "^5.0.0", + "npm-package-arg": "^9.0.0", + "npm-pick-manifest": "^7.0.2", + "npm-registry-fetch": "^13.0.0", + "npmlog": "^6.0.2", + "pacote": "^13.6.1", + "parse-conflict-json": "^2.0.1", + "proc-log": "^2.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.1", + "read-package-json-fast": "^2.0.2", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^9.0.0", + "treeverse": "^2.0.0", + "walk-up-path": "^1.0.0" + } + }, + "@npmcli/ci-detect": { + "version": "2.0.0", + "bundled": true + }, + "@npmcli/config": { + "version": "4.2.2", + "bundled": true, + "requires": { + "@npmcli/map-workspaces": "^2.0.2", + "ini": "^3.0.0", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^6.0.0", + "proc-log": "^2.0.0", + "read-package-json-fast": "^2.0.3", + "semver": "^7.3.5", + "walk-up-path": "^1.0.0" + } + }, + "@npmcli/disparity-colors": { + "version": "2.0.0", + "bundled": true, + "requires": { + "ansi-styles": "^4.3.0" + } + }, + "@npmcli/fs": { + "version": "2.1.2", + "bundled": true, + "requires": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + } + }, + "@npmcli/git": { + "version": "3.0.2", + "bundled": true, + "requires": { + "@npmcli/promise-spawn": "^3.0.0", + "lru-cache": "^7.4.4", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^7.0.0", + "proc-log": "^2.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + } + }, + "@npmcli/installed-package-contents": { + "version": "1.0.7", + "bundled": true, + "requires": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "dependencies": { + "npm-bundled": { + "version": "1.1.2", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + } + } + }, + "@npmcli/map-workspaces": { + "version": "2.0.4", + "bundled": true, + "requires": { + "@npmcli/name-from-folder": "^1.0.1", + "glob": "^8.0.1", + "minimatch": "^5.0.1", + "read-package-json-fast": "^2.0.3" + } + }, + "@npmcli/metavuln-calculator": { + "version": "3.1.1", + "bundled": true, + "requires": { + "cacache": "^16.0.0", + "json-parse-even-better-errors": "^2.3.1", + "pacote": "^13.0.3", + "semver": "^7.3.5" + } + }, + "@npmcli/move-file": { + "version": "2.0.1", + "bundled": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/name-from-folder": { + "version": "1.0.1", + "bundled": true + }, + "@npmcli/node-gyp": { + "version": "2.0.0", + "bundled": true + }, + "@npmcli/package-json": { + "version": "2.0.0", + "bundled": true, + "requires": { + "json-parse-even-better-errors": "^2.3.1" + } + }, + "@npmcli/promise-spawn": { + "version": "3.0.0", + "bundled": true, + "requires": { + "infer-owner": "^1.0.4" + } + }, + "@npmcli/query": { + "version": "1.2.0", + "bundled": true, + "requires": { + "npm-package-arg": "^9.1.0", + "postcss-selector-parser": "^6.0.10", + "semver": "^7.3.7" + } + }, + "@npmcli/run-script": { + "version": "4.2.1", + "bundled": true, + "requires": { + "@npmcli/node-gyp": "^2.0.0", + "@npmcli/promise-spawn": "^3.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^2.0.3", + "which": "^2.0.2" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "bundled": true + }, + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "agent-base": { + "version": "6.0.2", + "bundled": true, + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.2.1", + "bundled": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "bundled": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "bundled": true + }, + "ansi-styles": { + "version": "4.3.0", + "bundled": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "aproba": { + "version": "2.0.0", + "bundled": true + }, + "archy": { + "version": "1.0.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "3.0.1", + "bundled": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "asap": { + "version": "2.0.6", + "bundled": true + }, + "balanced-match": { + "version": "1.0.2", + "bundled": true + }, + "bin-links": { + "version": "3.0.3", + "bundled": true, + "requires": { + "cmd-shim": "^5.0.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0", + "read-cmd-shim": "^3.0.0", + "rimraf": "^3.0.0", + "write-file-atomic": "^4.0.0" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } + } + }, + "binary-extensions": { + "version": "2.2.0", + "bundled": true + }, + "brace-expansion": { + "version": "2.0.1", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "builtins": { + "version": "5.0.1", + "bundled": true, + "requires": { + "semver": "^7.0.0" + } + }, + "cacache": { + "version": "16.1.3", + "bundled": true, + "requires": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "bundled": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chownr": { + "version": "2.0.0", + "bundled": true + }, + "cidr-regex": { + "version": "3.1.1", + "bundled": true, + "requires": { + "ip-regex": "^4.1.0" + } + }, + "clean-stack": { + "version": "2.2.0", + "bundled": true + }, + "cli-columns": { + "version": "4.0.0", + "bundled": true, + "requires": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + } + }, + "cli-table3": { + "version": "0.6.2", + "bundled": true, + "requires": { + "@colors/colors": "1.5.0", + "string-width": "^4.2.0" + } + }, + "clone": { + "version": "1.0.4", + "bundled": true + }, + "cmd-shim": { + "version": "5.0.0", + "bundled": true, + "requires": { + "mkdirp-infer-owner": "^2.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "bundled": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "bundled": true + }, + "color-support": { + "version": "1.1.3", + "bundled": true + }, + "columnify": { + "version": "1.6.0", + "bundled": true, + "requires": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + } + }, + "common-ancestor-path": { + "version": "1.0.1", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "cssesc": { + "version": "3.0.0", + "bundled": true + }, + "debug": { + "version": "4.3.4", + "bundled": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "bundled": true + } + } + }, + "debuglog": { + "version": "1.0.1", + "bundled": true + }, + "defaults": { + "version": "1.0.3", + "bundled": true, + "requires": { + "clone": "^1.0.2" + } + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "depd": { + "version": "1.1.2", + "bundled": true + }, + "dezalgo": { + "version": "1.0.4", + "bundled": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "5.1.0", + "bundled": true + }, + "emoji-regex": { + "version": "8.0.0", + "bundled": true + }, + "encoding": { + "version": "0.1.13", + "bundled": true, + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + } + }, + "env-paths": { + "version": "2.2.1", + "bundled": true + }, + "err-code": { + "version": "2.0.3", + "bundled": true + }, + "fastest-levenshtein": { + "version": "1.0.12", + "bundled": true + }, + "fs-minipass": { + "version": "2.1.0", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "function-bind": { + "version": "1.1.1", + "bundled": true + }, + "gauge": { + "version": "4.0.4", + "bundled": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "glob": { + "version": "8.0.3", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "bundled": true + }, + "has": { + "version": "1.0.3", + "bundled": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "bundled": true + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hosted-git-info": { + "version": "5.2.1", + "bundled": true, + "requires": { + "lru-cache": "^7.5.1" + } + }, + "http-cache-semantics": { + "version": "4.1.1", + "bundled": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "bundled": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "bundled": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "humanize-ms": { + "version": "1.2.1", + "bundled": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.6.3", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ignore-walk": { + "version": "5.0.1", + "bundled": true, + "requires": { + "minimatch": "^5.0.1" + } + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true + }, + "indent-string": { + "version": "4.0.0", + "bundled": true + }, + "infer-owner": { + "version": "1.0.4", + "bundled": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true + }, + "ini": { + "version": "3.0.1", + "bundled": true + }, + "init-package-json": { + "version": "3.0.2", + "bundled": true, + "requires": { + "npm-package-arg": "^9.0.1", + "promzard": "^0.3.0", + "read": "^1.0.7", + "read-package-json": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^4.0.0" + } + }, + "ip": { + "version": "2.0.0", + "bundled": true + }, + "ip-regex": { + "version": "4.3.0", + "bundled": true + }, + "is-cidr": { + "version": "4.0.2", + "bundled": true, + "requires": { + "cidr-regex": "^3.1.1" + } + }, + "is-core-module": { + "version": "2.10.0", + "bundled": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "bundled": true + }, + "is-lambda": { + "version": "1.0.1", + "bundled": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "bundled": true + }, + "json-stringify-nice": { + "version": "1.1.4", + "bundled": true + }, + "jsonparse": { + "version": "1.3.1", + "bundled": true + }, + "just-diff": { + "version": "5.1.1", + "bundled": true + }, + "just-diff-apply": { + "version": "5.4.1", + "bundled": true + }, + "libnpmaccess": { + "version": "6.0.4", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "minipass": "^3.1.1", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmdiff": { + "version": "4.0.5", + "bundled": true, + "requires": { + "@npmcli/disparity-colors": "^2.0.0", + "@npmcli/installed-package-contents": "^1.0.7", + "binary-extensions": "^2.2.0", + "diff": "^5.1.0", + "minimatch": "^5.0.1", + "npm-package-arg": "^9.0.1", + "pacote": "^13.6.1", + "tar": "^6.1.0" + } + }, + "libnpmexec": { + "version": "4.0.14", + "bundled": true, + "requires": { + "@npmcli/arborist": "^5.6.3", + "@npmcli/ci-detect": "^2.0.0", + "@npmcli/fs": "^2.1.1", + "@npmcli/run-script": "^4.2.0", + "chalk": "^4.1.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-package-arg": "^9.0.1", + "npmlog": "^6.0.2", + "pacote": "^13.6.1", + "proc-log": "^2.0.0", + "read": "^1.0.7", + "read-package-json-fast": "^2.0.2", + "semver": "^7.3.7", + "walk-up-path": "^1.0.0" + } + }, + "libnpmfund": { + "version": "3.0.5", + "bundled": true, + "requires": { + "@npmcli/arborist": "^5.6.3" + } + }, + "libnpmhook": { + "version": "8.0.4", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmorg": { + "version": "4.0.4", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmpack": { + "version": "4.1.3", + "bundled": true, + "requires": { + "@npmcli/run-script": "^4.1.3", + "npm-package-arg": "^9.0.1", + "pacote": "^13.6.1" + } + }, + "libnpmpublish": { + "version": "6.0.5", + "bundled": true, + "requires": { + "normalize-package-data": "^4.0.0", + "npm-package-arg": "^9.0.1", + "npm-registry-fetch": "^13.0.0", + "semver": "^7.3.7", + "ssri": "^9.0.0" + } + }, + "libnpmsearch": { + "version": "5.0.4", + "bundled": true, + "requires": { + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmteam": { + "version": "4.0.4", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^13.0.0" + } + }, + "libnpmversion": { + "version": "3.0.7", + "bundled": true, + "requires": { + "@npmcli/git": "^3.0.0", + "@npmcli/run-script": "^4.1.3", + "json-parse-even-better-errors": "^2.3.1", + "proc-log": "^2.0.0", + "semver": "^7.3.7" + } + }, + "lru-cache": { + "version": "7.13.2", + "bundled": true + }, + "make-fetch-happen": { + "version": "10.2.1", + "bundled": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + } + }, + "minimatch": { + "version": "5.1.0", + "bundled": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "3.3.4", + "bundled": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "2.1.1", + "bundled": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + }, + "minipass-flush": { + "version": "1.0.5", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "bundled": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "bundled": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "bundled": true + }, + "mkdirp-infer-owner": { + "version": "2.0.0", + "bundled": true, + "requires": { + "chownr": "^2.0.0", + "infer-owner": "^1.0.4", + "mkdirp": "^1.0.3" + } + }, + "ms": { + "version": "2.1.3", + "bundled": true + }, + "mute-stream": { + "version": "0.0.8", + "bundled": true + }, + "negotiator": { + "version": "0.6.3", + "bundled": true + }, + "node-gyp": { + "version": "9.1.0", + "bundled": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "nopt": { + "version": "5.0.0", + "bundled": true, + "requires": { + "abbrev": "1" + } + } + } + }, + "nopt": { + "version": "6.0.0", + "bundled": true, + "requires": { + "abbrev": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "4.0.1", + "bundled": true, + "requires": { + "hosted-git-info": "^5.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + }, + "npm-audit-report": { + "version": "3.0.0", + "bundled": true, + "requires": { + "chalk": "^4.0.0" + } + }, + "npm-bundled": { + "version": "2.0.1", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^2.0.0" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } + } + }, + "npm-install-checks": { + "version": "5.0.0", + "bundled": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true + }, + "npm-package-arg": { + "version": "9.1.0", + "bundled": true, + "requires": { + "hosted-git-info": "^5.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "validate-npm-package-name": "^4.0.0" + } + }, + "npm-packlist": { + "version": "5.1.3", + "bundled": true, + "requires": { + "glob": "^8.0.1", + "ignore-walk": "^5.0.1", + "npm-bundled": "^2.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } + } + }, + "npm-pick-manifest": { + "version": "7.0.2", + "bundled": true, + "requires": { + "npm-install-checks": "^5.0.0", + "npm-normalize-package-bin": "^2.0.0", + "npm-package-arg": "^9.0.0", + "semver": "^7.3.5" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } + } + }, + "npm-profile": { + "version": "6.2.1", + "bundled": true, + "requires": { + "npm-registry-fetch": "^13.0.1", + "proc-log": "^2.0.0" + } + }, + "npm-registry-fetch": { + "version": "13.3.1", + "bundled": true, + "requires": { + "make-fetch-happen": "^10.0.6", + "minipass": "^3.1.6", + "minipass-fetch": "^2.0.3", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^9.0.1", + "proc-log": "^2.0.0" + } + }, + "npm-user-validate": { + "version": "1.0.1", + "bundled": true + }, + "npmlog": { + "version": "6.0.2", + "bundled": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.2", + "bundled": true + }, + "p-map": { + "version": "4.0.0", + "bundled": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "pacote": { + "version": "13.6.2", + "bundled": true, + "requires": { + "@npmcli/git": "^3.0.0", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/promise-spawn": "^3.0.0", + "@npmcli/run-script": "^4.1.0", + "cacache": "^16.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.6", + "mkdirp": "^1.0.4", + "npm-package-arg": "^9.0.0", + "npm-packlist": "^5.1.0", + "npm-pick-manifest": "^7.0.0", + "npm-registry-fetch": "^13.0.1", + "proc-log": "^2.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^5.0.0", + "read-package-json-fast": "^2.0.3", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11" + } + }, + "parse-conflict-json": { + "version": "2.0.2", + "bundled": true, + "requires": { + "json-parse-even-better-errors": "^2.3.1", + "just-diff": "^5.0.1", + "just-diff-apply": "^5.2.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "postcss-selector-parser": { + "version": "6.0.10", + "bundled": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "proc-log": { + "version": "2.0.1", + "bundled": true + }, + "promise-all-reject-late": { + "version": "1.0.1", + "bundled": true + }, + "promise-call-limit": { + "version": "1.0.1", + "bundled": true + }, + "promise-inflight": { + "version": "1.0.1", + "bundled": true + }, + "promise-retry": { + "version": "2.0.1", + "bundled": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "promzard": { + "version": "0.3.0", + "bundled": true, + "requires": { + "read": "1" + } + }, + "qrcode-terminal": { + "version": "0.12.0", + "bundled": true + }, + "read": { + "version": "1.0.7", + "bundled": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cmd-shim": { + "version": "3.0.0", + "bundled": true + }, + "read-package-json": { + "version": "5.0.2", + "bundled": true, + "requires": { + "glob": "^8.0.1", + "json-parse-even-better-errors": "^2.3.1", + "normalize-package-data": "^4.0.0", + "npm-normalize-package-bin": "^2.0.0" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } + } + }, + "read-package-json-fast": { + "version": "2.0.3", + "bundled": true, + "requires": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "bundled": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "bundled": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "retry": { + "version": "0.12.0", + "bundled": true + }, + "rimraf": { + "version": "3.0.2", + "bundled": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "semver": { + "version": "7.3.7", + "bundled": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "bundled": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.7", + "bundled": true + }, + "smart-buffer": { + "version": "4.2.0", + "bundled": true + }, + "socks": { + "version": "2.7.0", + "bundled": true, + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "7.0.0", + "bundled": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + }, + "spdx-correct": { + "version": "3.1.1", + "bundled": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "bundled": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "bundled": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.11", + "bundled": true + }, + "ssri": { + "version": "9.0.1", + "bundled": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "bundled": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "bundled": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "bundled": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tar": { + "version": "6.1.11", + "bundled": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "bundled": true + }, + "tiny-relative-date": { + "version": "1.3.0", + "bundled": true + }, + "treeverse": { + "version": "2.0.0", + "bundled": true + }, + "unique-filename": { + "version": "2.0.1", + "bundled": true, + "requires": { + "unique-slug": "^3.0.0" + } + }, + "unique-slug": { + "version": "3.0.0", + "bundled": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "bundled": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "4.0.0", + "bundled": true, + "requires": { + "builtins": "^5.0.0" + } + }, + "walk-up-path": { + "version": "1.0.0", + "bundled": true + }, + "wcwidth": { + "version": "1.0.1", + "bundled": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "which": { + "version": "2.0.2", + "bundled": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wide-align": { + "version": "1.1.5", + "bundled": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "write-file-atomic": { + "version": "4.0.2", + "bundled": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "yallist": { + "version": "4.0.0", + "bundled": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==" + }, + "p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkg": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "requires": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "requires": { + "resolve": "^1.1.6" + } + }, + "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==", + "dev": true + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "dependencies": { + "is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "requires": { + "has": "^1.0.3" + } + } + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "requires": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "requires": { + "readable-stream": "^2.1.4" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "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==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==", + "requires": { + "os-homedir": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "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==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "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==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } +} diff --git a/standalone/package.json b/standalone/package.json index de8eeefa60e..6c24c06ec7c 100644 --- a/standalone/package.json +++ b/standalone/package.json @@ -12,14 +12,15 @@ "license": "MIT", "dependencies": { "chalk": "^2.4.2", - "npm": "^6.10.2", + "npm": "^8.19.0", "shelljs": "^0.8.3", "shx": "^0.3.2", "user-home": "^2.0.0" }, "pkg": { "scripts": [ - "node_modules/npm/src/**/*.js" + "node_modules/npm/lib/*.js", + "node_modules/npm/lib/**/*.js" ], "assets": [ "node_modules/.bin/**", @@ -29,7 +30,7 @@ ] }, "devDependencies": { - "pkg": "^4.4.2", + "pkg": "^5.7.0", "prettier": "^1.15.3" } } diff --git a/templates/_gitignore b/templates/_gitignore index dbb58ffbfa3..b17f6310755 100644 --- a/templates/_gitignore +++ b/templates/_gitignore @@ -64,3 +64,6 @@ node_modules/ # dotenv environment variables file .env + +# dataconnect generated files +.dataconnect diff --git a/templates/emulators/default_storage.rules b/templates/emulators/default_storage.rules new file mode 100644 index 00000000000..80c50ce59be --- /dev/null +++ b/templates/emulators/default_storage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write; + } + } +} diff --git a/templates/extensions/CHANGELOG.md b/templates/extensions/CHANGELOG.md deleted file mode 100644 index ae0d89e5d61..00000000000 --- a/templates/extensions/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -CHANGELOG.md is how you tell users what has changed in each version of your extension. -When you release a new version, add a new header and release notes for that version. -When users update their instance, they will see the release notes for all versions -between the one they were on and the one they are updating to. - -## Version 0.0.1 -First version \ No newline at end of file diff --git a/templates/extensions/CL-template.md b/templates/extensions/CL-template.md new file mode 100644 index 00000000000..6ba712636d1 --- /dev/null +++ b/templates/extensions/CL-template.md @@ -0,0 +1,2 @@ +## Version 0.0.1 +- Initial Version \ No newline at end of file diff --git a/templates/extensions/POSTINSTALL.md b/templates/extensions/POSTINSTALL.md index 9e42cd63403..9fc028eeb2a 100644 --- a/templates/extensions/POSTINSTALL.md +++ b/templates/extensions/POSTINSTALL.md @@ -4,10 +4,10 @@ This file provides your users an overview of how to use your extension after the Include instructions for using the extension and any important functional details. Also include **detailed descriptions** for any additional post-installation setup required by the user. Reference values for the extension instance using the ${param:PARAMETER_NAME} or ${function:VARIABLE_NAME} syntax. -Learn more in the docs: https://firebase.google.com/docs/extensions/alpha/create-user-docs#reference-in-postinstall +Learn more in the docs: https://firebase.google.com/docs/extensions/publishers/user-documentation#reference-in-postinstall Learn more about writing a POSTINSTALL.md file in the docs: -https://firebase.google.com/docs/extensions/alpha/create-user-docs#writing-postinstall +https://firebase.google.com/docs/extensions/publishers/user-documentation#writing-postinstall --> # See it in action diff --git a/templates/extensions/PREINSTALL.md b/templates/extensions/PREINSTALL.md index 89bd90b5d3c..5cfd615f190 100644 --- a/templates/extensions/PREINSTALL.md +++ b/templates/extensions/PREINSTALL.md @@ -4,7 +4,7 @@ This file provides your users an overview of your extension. All content is opti Include any important functional details as well as a brief description for any additional setup required by the user (both pre- and post-installation). Learn more about writing a PREINSTALL.md file in the docs: -https://firebase.google.com/docs/extensions/alpha/create-user-docs#writing-preinstall +https://firebase.google.com/docs/extensions/publishers/user-documentation#writing-preinstall --> Use this extension to send a friendly greeting. diff --git a/templates/extensions/extension.yaml b/templates/extensions/extension.yaml index 4169806243b..b34258ef9d8 100644 --- a/templates/extensions/extension.yaml +++ b/templates/extensions/extension.yaml @@ -1,7 +1,9 @@ # Learn detailed information about the fields of an extension.yaml file in the docs: -# https://firebase.google.com/docs/extensions/alpha/ref-extension-yaml +# https://firebase.google.com/docs/extensions/reference/extension-yaml -name: greet-the-world # Identifier for your extension +# Identifier for your extension +# TODO: Replace this with an descriptive name for your extension. +name: greet-the-world version: 0.0.1 # Follow semver versioning specVersion: v1beta # Version of the Firebase Extensions specification @@ -14,36 +16,39 @@ description: >- license: Apache-2.0 # https://spdx.org/licenses/ -# Public URL for the source code of your extension -sourceUrl: https://github.com/firebase/firebase-tools/tree/master/templates/extensions +# Public URL for the source code of your extension. +# TODO: Replace this with your GitHub repo. +sourceUrl: https://github.com/ORG_OR_USER/REPO_NAME # Specify whether a paid-tier billing plan is required to use your extension. -# Learn more in the docs: https://firebase.google.com/docs/extensions/alpha/ref-extension-yaml#billing-required-field -billingRequired: false +# Learn more in the docs: https://firebase.google.com/docs/extensions/reference/extension-yaml#billing-required-field +billingRequired: true # In an `apis` field, list any Google APIs (like Cloud Translation, BigQuery, etc.) # required for your extension to operate. -# Learn more in the docs: https://firebase.google.com/docs/extensions/alpha/ref-extension-yaml#apis-field +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#apis-field # In a `roles` field, list any IAM access roles required for your extension to operate. -# Learn more in the docs: https://firebase.google.com/docs/extensions/alpha/ref-extension-yaml#roles-field +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#roles-field # In the `resources` field, list each of your extension's functions, including the trigger for each function. -# Learn more in the docs: https://firebase.google.com/docs/extensions/alpha/ref-extension-yaml#resources-field +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#resources-field resources: - name: greetTheWorld type: firebaseextensions.v1beta.function description: >- HTTP request-triggered function that responds with a specified greeting message properties: - # LOCATION is a user-configured parameter value specified by the user during installation. - location: ${LOCATION} # httpsTrigger is used for an HTTP triggered function. httpsTrigger: {} - runtime: "nodejs12" + runtime: "nodejs16" # In the `params` field, set up your extension's user-configured parameters. -# Learn more in the docs: https://firebase.google.com/docs/extensions/alpha/ref-extension-yaml#params-field +# Learn more in the docs: +# https://firebase.google.com/docs/extensions/reference/extension-yaml#params-field params: - param: GREETING label: Greeting for the world @@ -54,54 +59,3 @@ params: default: Hello required: true immutable: false - - - param: LOCATION - label: Cloud Functions location - description: >- - Where do you want to deploy the functions created for this extension? - For help selecting a location, refer to the [location selection - guide](https://firebase.google.com/docs/functions/locations). - type: select - options: - - label: Iowa (us-central1) - value: us-central1 - - label: South Carolina (us-east1) - value: us-east1 - - label: Northern Virginia (us-east4) - value: us-east4 - - label: Los Angeles (us-west2) - value: us-west2 - - label: Salt Lake City (us-west3) - value: us-west3 - - label: Las Vegas (us-west4) - value: us-west4 - - label: Warsaw (europe-central2) - value: europe-central2 - - label: Belgium (europe-west1) - value: europe-west1 - - label: London (europe-west2) - value: europe-west2 - - label: Frankfurt (europe-west3) - value: europe-west3 - - label: Zurich (europe-west6) - value: europe-west6 - - label: Hong Kong (asia-east2) - value: asia-east2 - - label: Tokyo (asia-northeast1) - value: asia-northeast1 - - label: Osaka (asia-northeast2) - value: asia-northeast2 - - label: Seoul (asia-northeast3) - value: asia-northeast3 - - label: Mumbai (asia-south1) - value: asia-south1 - - label: Jakarta (asia-southeast2) - value: asia-southeast2 - - label: Montreal (northamerica-northeast1) - value: northamerica-northeast1 - - label: Sao Paulo (southamerica-east1) - value: southamerica-east1 - - label: Sydney (australia-southeast1) - value: australia-southeast1 - required: true - immutable: true diff --git a/templates/extensions/integration-test.env b/templates/extensions/integration-test.env new file mode 100644 index 00000000000..a90ed76eb87 --- /dev/null +++ b/templates/extensions/integration-test.env @@ -0,0 +1,2 @@ +GREETING=Hello +LOCATION=us-central1 \ No newline at end of file diff --git a/templates/extensions/integration-test.json b/templates/extensions/integration-test.json new file mode 100644 index 00000000000..81dfc0b04c3 --- /dev/null +++ b/templates/extensions/integration-test.json @@ -0,0 +1,14 @@ +{ + "emulators": { + "functions": { + "port": 5001 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true + }, + "extensions": { + "greet-the-world": "../.." + } +} diff --git a/templates/extensions/javascript/WELCOME.md b/templates/extensions/javascript/WELCOME.md index c4ec21c9f08..e5a76a8dafc 100644 --- a/templates/extensions/javascript/WELCOME.md +++ b/templates/extensions/javascript/WELCOME.md @@ -1,9 +1,18 @@ -This directory now contains the source files for a simple extension called **greet-the-world**. To try out this extension right away, install it in an existing Firebase project by running: +This directory now contains the source files for a simple extension called **greet-the-world**. You can try it out right away in the Firebase Emulator suite - just navigate to the integration-test directory and run: -`firebase ext:install . --project=` +`firebase emulators:start --project=` -If you want to jump into the code to customize your extension, then modify **index.js** and **extension.yaml** in your favorite editor. When you're ready to try out your fancy new extension, run: +If you don't have a project to use, you can instead use '--project=demo-test' to run against a fake project. -`firebase ext:install . --project=` +The `integration-test` directory also includes an end to end test (in the file integration-test.spec.js) that verifies that the extension responds back with the expected greeting. You can see it in action by running: -As always, in the docs, you can find detailed instructions for creating and testing your extension (including using the emulator!). +`npm run test` + +If you want to jump into the code to customize your extension, then modify **index.js** and **extension.yaml** in your favorite editor. + +If you want to deploy your extension to test on a real project, go to a Firebase project directory (or create a new one with `firebase init`) and run: + +`firebase ext:install ./path/to/extension/directory --project=` +`firebase deploy --only extensions` + +You can find more information about building extensions in the publisher docs: https://firebase.google.com/docs/extensions/publishers/get-started diff --git a/templates/extensions/javascript/index.js b/templates/extensions/javascript/index.js index a75b9de5898..3bd6dc6d4fb 100644 --- a/templates/extensions/javascript/index.js +++ b/templates/extensions/javascript/index.js @@ -1,22 +1,22 @@ /* - * This template contains a HTTP function that responds with a greeting when called - * - * Always use the FUNCTIONS HANDLER NAMESPACE - * when writing Cloud Functions for extensions. - * Learn more about the handler namespace in the docs + * This template contains a HTTP function that + * responds with a greeting when called * * Reference PARAMETERS in your functions code with: * `process.env.` - * Learn more about parameters in the docs + * Learn more about building extensions in the docs: + * https://firebase.google.com/docs/extensions/publishers */ -const functions = require('firebase-functions'); +const functions = require("firebase-functions"); -exports.greetTheWorld = functions.handler.https.onRequest((req, res) => { - // Here we reference a user-provided parameter (its value is provided by the user during installation) +exports.greetTheWorld = functions.https.onRequest((req, res) => { + // Here we reference a user-provided parameter + // (its value is provided by the user during installation) const consumerProvidedGreeting = process.env.GREETING; - // And here we reference an auto-populated parameter (its value is provided by Firebase after installation) + // And here we reference an auto-populated parameter + // (its value is provided by Firebase after installation) const instanceId = process.env.EXT_INSTANCE_ID; const greeting = `${consumerProvidedGreeting} World from ${instanceId}`; diff --git a/templates/extensions/javascript/integration-test.js b/templates/extensions/javascript/integration-test.js new file mode 100644 index 00000000000..959bc3a82cb --- /dev/null +++ b/templates/extensions/javascript/integration-test.js @@ -0,0 +1,13 @@ +const axios = require("axios"); +const chai = require("chai"); + +describe("greet-the-world", () => { + it("should respond with the configured greeting", async () => { + const expected = "Hello World from greet-the-world"; + + const httpFunctionUri = "http://localhost:5001/demo-test/us-central1/ext-greet-the-world-greetTheWorld/"; + const res = await axios.get(httpFunctionUri); + + return chai.expect(res.data).to.eql(expected); + }).timeout(10000); +}); diff --git a/templates/extensions/javascript/package.lint.json b/templates/extensions/javascript/package.lint.json index b023cee30b0..73c25633ae6 100644 --- a/templates/extensions/javascript/package.lint.json +++ b/templates/extensions/javascript/package.lint.json @@ -3,15 +3,23 @@ "description": "Greet the world", "main": "index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" }, "devDependencies": { - "eslint": "^4.13.1", - "eslint-plugin-promise": "^3.6.0" + "eslint": "^8.15.1", + "eslint-plugin-promise": "^6.0.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-import": "^2.25.4", + "axios": "^1.3.2", + "chai": "^4.3.7", + "mocha": "^10.2.0" }, "scripts": { - "lint": "./node_modules/.bin/eslint --max-warnings=0 .." + "lint": "./node_modules/.bin/eslint --max-warnings=0 ..", + "lint:fix": "./node_modules/.bin/eslint --max-warnings=0 --fix ..", + "mocha": "mocha '**/*.spec.js'", + "test": "(cd integration-tests && firebase emulators:exec 'npm run mocha' -P demo-test)" }, "private": true -} +} \ No newline at end of file diff --git a/templates/extensions/javascript/package.nolint.json b/templates/extensions/javascript/package.nolint.json index 7624b2f57e3..6c83869c94d 100644 --- a/templates/extensions/javascript/package.nolint.json +++ b/templates/extensions/javascript/package.nolint.json @@ -3,8 +3,17 @@ "description": "Greet the world", "main": "index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "devDependencies": { + "axios": "^1.3.2", + "chai": "^4.3.7", + "mocha": "^10.2.0" + }, + "scripts": { + "mocha": "mocha '**/*.spec.js'", + "test": "(cd integration-tests && firebase emulators:exec 'npm run mocha' -P demo-test)" }, "private": true -} +} \ No newline at end of file diff --git a/templates/extensions/typescript/WELCOME.md b/templates/extensions/typescript/WELCOME.md index a0635c860a2..57895a0c57e 100644 --- a/templates/extensions/typescript/WELCOME.md +++ b/templates/extensions/typescript/WELCOME.md @@ -1,9 +1,22 @@ -This directory now contains the source files for a simple extension called **greet-the-world**. To try out this extension right away, install it in an existing Firebase project by running: +This directory now contains the source files for a simple extension called **greet-the-world**. You can try it out right away in the Firebase Emulator suite: first, compile your code by running: -`npm run build --prefix=functions && firebase ext:install . --project=` +`npm run build --prefix=functions` -If you want to jump into the code to customize your extension, then modify **index.ts** and **extension.yaml** in your favorite editor. When you're ready to try out your fancy new extension, run: +Then, navigate to the `functions/integration-test` directory and run: -`npm run build --prefix=functions && firebase ext:install . --project=` +`firebase emulators:start --project=` -As always, in the docs, you can find detailed instructions for creating and testing your extension (including using the emulator!). +If you don't have a project to use, you can instead use '--project=demo-test' to run against a fake project. + +The `integration-test` directory also includes an end to end test (in the file **integration-test.spec.ts**) that verifies that the extension responds back with the expected greeting. You can see it in action by running: + +`npm run test` + +If you want to jump into the code to customize your extension, then modify **index.ts** and **extension.yaml** in your favorite editor. + +If you want to deploy your extension to test on a real project, go to a Firebase project directory (or create a new one with `firebase init`) and run: + +`firebase ext:install ./path/to/extension/directory --project=` +`firebase deploy --only extensions` + +You can find more information about building extensions in the publisher docs: https://firebase.google.com/docs/extensions/publishers/get-started diff --git a/templates/extensions/typescript/_mocharc b/templates/extensions/typescript/_mocharc new file mode 100644 index 00000000000..4d837556b90 --- /dev/null +++ b/templates/extensions/typescript/_mocharc @@ -0,0 +1,10 @@ +{ + "require": "ts-node/register", + "extensions": ["ts", "tsx"], + "spec": [ + "integration-tests/**/*.spec.*" + ], + "watch-files": [ + "src" + ] +} \ No newline at end of file diff --git a/templates/extensions/typescript/index.ts b/templates/extensions/typescript/index.ts index 3c5151e7602..56462c43dc3 100644 --- a/templates/extensions/typescript/index.ts +++ b/templates/extensions/typescript/index.ts @@ -1,25 +1,26 @@ /* - * This template contains a HTTP function that responds with a greeting when called - * - * Always use the FUNCTIONS HANDLER NAMESPACE - * when writing Cloud Functions for extensions. - * Learn more about the handler namespace in the docs + * This template contains a HTTP function that responds + * with a greeting when called * * Reference PARAMETERS in your functions code with: * `process.env.` - * Learn more about parameters in the docs + * Learn more about building extensions in the docs: + * https://firebase.google.com/docs/extensions/publishers */ -import * as functions from 'firebase-functions'; +import * as functions from "firebase-functions"; -exports.greetTheWorld = functions.handler.https.onRequest((req, res) => { - // Here we reference a user-provided parameter (its value is provided by the user during installation) - const consumerProvidedGreeting = process.env.GREETING; +exports.greetTheWorld = functions.https.onRequest( + (req: functions.Request, res: functions.Response) => { + // Here we reference a user-provided parameter + // (its value is provided by the user during installation) + const consumerProvidedGreeting = process.env.GREETING; - // And here we reference an auto-populated parameter (its value is provided by Firebase after installation) - const instanceId = process.env.EXT_INSTANCE_ID; + // And here we reference an auto-populated parameter + // (its value is provided by Firebase after installation) + const instanceId = process.env.EXT_INSTANCE_ID; - const greeting = `${consumerProvidedGreeting} World from ${instanceId}`; + const greeting = `${consumerProvidedGreeting} World from ${instanceId}`; - res.send(greeting); -}); + res.send(greeting); + }); diff --git a/templates/extensions/typescript/integration-test.ts b/templates/extensions/typescript/integration-test.ts new file mode 100644 index 00000000000..ca06f775a02 --- /dev/null +++ b/templates/extensions/typescript/integration-test.ts @@ -0,0 +1,13 @@ +import axios from "axios"; +import { expect } from "chai"; + +describe("greet-the-world", () => { + it("should respond with the configured greeting", async () => { + const expected = "Hello World from greet-the-world"; + + const httpFunctionUri = "http://localhost:5001/demo-test/us-central1/ext-greet-the-world-greetTheWorld/"; + const res = await axios.get(httpFunctionUri); + + return expect(res.data).to.eql(expected); + }).timeout(10000); +}); diff --git a/templates/extensions/typescript/package.lint.json b/templates/extensions/typescript/package.lint.json index 7dc16acf965..05f17ce4e7a 100644 --- a/templates/extensions/typescript/package.lint.json +++ b/templates/extensions/typescript/package.lint.json @@ -2,17 +2,30 @@ "name": "functions", "scripts": { "lint": "eslint \"src/**/*\"", - "build": "tsc" + "lint:fix": "eslint \"src/**/*\" --fix", + "build": "tsc", + "build:watch": "tsc --watch", + "mocha": "mocha '**/*.spec.ts'", + "test": "(cd integration-tests && firebase emulators:exec 'npm run mocha' -P demo-test)" }, "main": "lib/index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" }, "devDependencies": { - "eslint": "^7.6.0", - "eslint-plugin-import": "^2.22.0", - "typescript": "^3.8.0" + "@types/chai": "^4.3.4", + "@types/mocha": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^5.12.0", + "@typescript-eslint/parser": "^5.12.0", + "eslint": "^8.15.1", + "eslint-plugin-import": "^2.26.0", + "eslint-config-google": "^0.14.0", + "typescript": "^4.9.0", + "axios": "^1.3.2", + "chai": "^4.3.7", + "mocha": "^10.2.0", + "ts-node": "^10.4.0" }, "private": true -} +} \ No newline at end of file diff --git a/templates/extensions/typescript/package.nolint.json b/templates/extensions/typescript/package.nolint.json index 01d543f37a0..06708498ad5 100644 --- a/templates/extensions/typescript/package.nolint.json +++ b/templates/extensions/typescript/package.nolint.json @@ -1,15 +1,24 @@ { "name": "functions", "scripts": { - "build": "tsc" + "build": "tsc", + "build:watch": "tsc --watch", + "mocha": "mocha '**/*.spec.ts'", + "test": "(cd integration-tests && firebase emulators:exec 'npm run mocha' -P demo-test)" }, "main": "lib/index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" }, "devDependencies": { - "typescript": "^3.8.0" + "@types/chai": "^4.3.4", + "@types/mocha": "^10.0.1", + "typescript": "^4.9.0", + "axios": "^1.3.2", + "chai": "^4.3.7", + "mocha": "^10.2.0", + "ts-node": "^10.4.0" }, "private": true } diff --git a/templates/hosting/init.js b/templates/hosting/init.js index b2192f4a02c..e2f04ce75bc 100644 --- a/templates/hosting/init.js +++ b/templates/hosting/init.js @@ -6,8 +6,8 @@ if (firebaseConfig) { /*--EMULATORS--*/ if (firebaseEmulators) { console.log("Automatically connecting Firebase SDKs to running emulators:"); - Object.keys(firebaseEmulators).forEach(function(key) { - console.log('\t' + key + ': http://' + firebaseEmulators[key].host + ':' + firebaseEmulators[key].port ); + Object.keys(firebaseEmulators).forEach(function (key) { + console.log('\t' + key + ': http://' + firebaseEmulators[key].hostAndPort); }); if (firebaseEmulators.database && typeof firebase.database === 'function') { @@ -23,7 +23,12 @@ if (firebaseConfig) { } if (firebaseEmulators.auth && typeof firebase.auth === 'function') { - firebase.auth().useEmulator('http://' + firebaseEmulators.auth.host + ':' + firebaseEmulators.auth.port); + // TODO: Consider using location.protocol + '//' instead (may help HTTPS). + firebase.auth().useEmulator('http://' + firebaseEmulators.auth.hostAndPort); + } + + if (firebaseEmulators.storage && typeof firebase.storage === 'function') { + firebase.storage().useEmulator(firebaseEmulators.storage.host, firebaseEmulators.storage.port); } } else { console.log("To automatically connect the Firebase SDKs to running emulators, replace '/__/firebase/init.js' with '/__/firebase/init.js?useEmulator=true' in your index.html"); diff --git a/templates/init/dataconnect/connector.yaml b/templates/init/dataconnect/connector.yaml new file mode 100644 index 00000000000..de62ea7766b --- /dev/null +++ b/templates/init/dataconnect/connector.yaml @@ -0,0 +1,13 @@ +connectorId: "__connectorId__" +authMode: "PUBLIC" +## ## Here's an example of how to add generated SDKs. +## ## You'll need to replace the outputDirs with ones pointing to where you want the generated code in your app. +# generate: +# javascriptSdk: +# outputDir: +# package: "@firebasegen/my-connector" +# packageJSONDir: < Optional. Path to your Javascript app's package.json> +# swiftSdk: +# outputDir: +# kotlinSdk: +# outputDir: diff --git a/templates/init/dataconnect/dataconnect.yaml b/templates/init/dataconnect/dataconnect.yaml new file mode 100644 index 00000000000..e48139fb38e --- /dev/null +++ b/templates/init/dataconnect/dataconnect.yaml @@ -0,0 +1,11 @@ +specVersion: "v1alpha" +serviceId: "__serviceId__" +location: "__location__" +schema: + source: "./schema" + datasource: + postgresql: + database: "__cloudSqlDatabase__" + cloudSql: + instanceId: "__cloudSqlInstanceId__" +connectorDirs: ["./__connectorId__"] diff --git a/templates/init/dataconnect/mutations.gql b/templates/init/dataconnect/mutations.gql new file mode 100644 index 00000000000..d2fb9a13283 --- /dev/null +++ b/templates/init/dataconnect/mutations.gql @@ -0,0 +1,35 @@ +# # Example mutations for a simple email app + +# # Logged in user can create their own account. +# mutation CreateUser($name: String!, $address: String!) @auth(level: USER) { +# # _insert lets you create a new row in your table. +# user_insert(data: { +# # Server values let your service populate sensitive data. +# # Users can only setup their own account. +# uid_expr: "auth.uid", +# name: $name, +# address: $address +# }) +# } + +# # Logged in user can send emails from their account. +# mutation CreateEmail($content: String, $subject: String) @auth(level: USER) { +# email_insert(data: { +# # The request variable name doesn't have to match the field name. +# text: $content, +# subject: $subject, +# # Server values let your service populate sensitive data. +# # Users are only allowed to create emails sent from their account. +# fromUid_expr: "auth.uid", +# # Server values let your service populate data for you +# # Here, we use sent_date: { today: true } to set 'sent' to today's date. +# sent_date: { today: true } +# }) +# } + +# mutation CreateRecipient($emailId: UUID) @auth(level: USER) { +# recipient_insert(data: { +# emailId: $emailId, +# userUid_expr: "auth.uid" +# }) +# } diff --git a/templates/init/dataconnect/queries.gql b/templates/init/dataconnect/queries.gql new file mode 100644 index 00000000000..e099f5563a3 --- /dev/null +++ b/templates/init/dataconnect/queries.gql @@ -0,0 +1,56 @@ +# # Example queries for a simple email app. + +# # @auth() directives control who can call each operation. +# # Only admins should be able to list all emails, so we use NO_ACCESS +# query ListEmails @auth(level: NO_ACCESS) { +# emails { +# id, subject, text, sent +# from { +# name +# } +# } +# } + +# # Only admins should be able to list all users, so we use NO_ACCESS +# query ListUsers @auth(level: NO_ACCESS) { +# users { uid, name, address } +# } + +# # Logged in users should be able to see their inbox though, so we use USER +# query ListInbox @auth(level: USER) { +# # where allows you to filter lists +# # Here, we use it to filter to only emails that are sent to the logged in user. +# emails(where: { +# users_via_Recipient: { +# exist: { uid: { eq_expr: "auth.uid" } +# }} +# }) { +# id subject sent +# content: text # Select the `text` field but alias it as `content` in the response. +# sender: from { name address uid } +# # _on_ makes it easy to grab info from another table +# # Here, we use it to grab all the recipients of the email. +# to: recipients_on_email { +# user { name address uid } +# } +# } +# } + +# query GetUidByEmail($emails: [String!]) @auth(level: PUBLIC) { +# users(where: { address: { in: $emails } }) { +# uid address +# } +# } + +# query ListSent($uid: String) @auth(level: PUBLIC) { +# emails(where: { +# from: {uid: {eq: $uid }} +# }) { +# id subject sent +# content: text +# sender: from { name address uid } +# to: recipients_on_email { +# user { name address uid } +# } +# } +# } diff --git a/templates/init/dataconnect/schema.gql b/templates/init/dataconnect/schema.gql new file mode 100644 index 00000000000..31d751240e4 --- /dev/null +++ b/templates/init/dataconnect/schema.gql @@ -0,0 +1,28 @@ +# # Example schema for simple email app +# type User @table(key: "uid") { +# uid: String! +# name: String! +# address: String! +# } + +# type Email @table { +# subject: String! +# sent: Date! +# text: String! +# from: User! +# } + +# type Recipient @table(key: ["email", "user"]) { +# email: Email! +# user: User! +# } + +# type EmailMeta @table(key: ["user", "email"]) { +# user: User! +# email: Email! +# labels: [String] +# read: Boolean! +# starred: Boolean! +# muted: Boolean! +# snoozed: Date +# } diff --git a/templates/init/functions/golang/_gitignore b/templates/init/functions/golang/_gitignore deleted file mode 100644 index f2dd9554a12..00000000000 --- a/templates/init/functions/golang/_gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out diff --git a/templates/init/functions/golang/functions.go b/templates/init/functions/golang/functions.go deleted file mode 100644 index 9b44a27868b..00000000000 --- a/templates/init/functions/golang/functions.go +++ /dev/null @@ -1,38 +0,0 @@ -package PACKAGE - -// Welcome to Cloud Functions for Firebase for Golang! -// To get started, simply uncomment the below code or create your own. -// Deploy with `firebase deploy` - -/* -import ( - "context" - "fmt" - - "github.com/FirebaseExtended/firebase-functions-go/https" - "github.com/FirebaseExtended/firebase-functions-go/pubsub" - "github.com/FirebaseExtended/firebase-functions-go/runwith" -) - -var HelloWorld = https.Function{ - RunWith: https.Options{ - AvailableMemoryMB: 256, - }, - Callback: func(w https.ResponseWriter, req *https.Request) { - fmt.Println("Hello, world!") - fmt.Fprintf(w, "Hello, world!\n") - }, -} - -var PubSubFunction = pubsub.Function{ - EventType: pubsub.MessagePublished, - Topic: "topic", - RunWith: runwith.Options{ - AvailableMemoryMB: 256, - }, - Callback: func(ctx context.Context, message pubsub.Message) error { - fmt.Printf("Got Pub/Sub event %+v", message) - return nil - }, -} -*/ diff --git a/templates/init/functions/javascript/_eslintrc b/templates/init/functions/javascript/_eslintrc index 973030b23cc..f4cb76caae2 100644 --- a/templates/init/functions/javascript/_eslintrc +++ b/templates/init/functions/javascript/_eslintrc @@ -1,14 +1,28 @@ module.exports = { - root: true, env: { es6: true, node: true, }, + parserOptions: { + "ecmaVersion": 2018, + }, extends: [ "eslint:recommended", "google", ], rules: { - quotes: ["error", "double"], + "no-restricted-globals": ["error", "name", "length"], + "prefer-arrow-callback": "error", + "quotes": ["error", "double", {"allowTemplateLiterals": true}], }, + overrides: [ + { + files: ["**/*.spec.*"], + env: { + mocha: true, + }, + rules: {}, + }, + ], + globals: {}, }; diff --git a/templates/init/functions/javascript/_gitignore b/templates/init/functions/javascript/_gitignore index 40b878db5b1..21ee8d3d1d8 100644 --- a/templates/init/functions/javascript/_gitignore +++ b/templates/init/functions/javascript/_gitignore @@ -1 +1,2 @@ -node_modules/ \ No newline at end of file +node_modules/ +*.local \ No newline at end of file diff --git a/templates/init/functions/javascript/index.js b/templates/init/functions/javascript/index.js index 081873b3f9b..e81477f69a0 100644 --- a/templates/init/functions/javascript/index.js +++ b/templates/init/functions/javascript/index.js @@ -1,9 +1,19 @@ -const functions = require("firebase-functions"); +/** + * Import function triggers from their respective submodules: + * + * const {onCall} = require("firebase-functions/v2/https"); + * const {onDocumentWritten} = require("firebase-functions/v2/firestore"); + * + * See a full list of supported triggers at https://firebase.google.com/docs/functions + */ -// // Create and Deploy Your First Cloud Functions -// // https://firebase.google.com/docs/functions/write-firebase-functions -// -// exports.helloWorld = functions.https.onRequest((request, response) => { -// functions.logger.info("Hello logs!", {structuredData: true}); +const {onRequest} = require("firebase-functions/v2/https"); +const logger = require("firebase-functions/logger"); + +// Create and deploy your first functions +// https://firebase.google.com/docs/functions/get-started + +// exports.helloWorld = onRequest((request, response) => { +// logger.info("Hello logs!", {structuredData: true}); // response.send("Hello from Firebase!"); // }); diff --git a/templates/init/functions/javascript/package.lint.json b/templates/init/functions/javascript/package.lint.json index f952eb8ad11..071683feb3d 100644 --- a/templates/init/functions/javascript/package.lint.json +++ b/templates/init/functions/javascript/package.lint.json @@ -10,17 +10,17 @@ "logs": "firebase functions:log" }, "engines": { - "node": "16" + "node": "18" }, "main": "index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" }, "devDependencies": { - "eslint": "^7.6.0", + "eslint": "^8.15.0", "eslint-config-google": "^0.14.0", - "firebase-functions-test": "^0.2.0" + "firebase-functions-test": "^3.1.0" }, "private": true } diff --git a/templates/init/functions/javascript/package.nolint.json b/templates/init/functions/javascript/package.nolint.json index 1dae83651f9..2e86341e08d 100644 --- a/templates/init/functions/javascript/package.nolint.json +++ b/templates/init/functions/javascript/package.nolint.json @@ -9,15 +9,15 @@ "logs": "firebase functions:log" }, "engines": { - "node": "16" + "node": "18" }, "main": "index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" }, "devDependencies": { - "firebase-functions-test": "^0.2.0" + "firebase-functions-test": "^3.1.0" }, "private": true } diff --git a/templates/init/functions/python/_gitignore b/templates/init/functions/python/_gitignore new file mode 100644 index 00000000000..e45d6e35639 --- /dev/null +++ b/templates/init/functions/python/_gitignore @@ -0,0 +1 @@ +*.local \ No newline at end of file diff --git a/templates/init/functions/python/main.py b/templates/init/functions/python/main.py new file mode 100644 index 00000000000..1d2add1e26a --- /dev/null +++ b/templates/init/functions/python/main.py @@ -0,0 +1,13 @@ +# Welcome to Cloud Functions for Firebase for Python! +# To get started, simply uncomment the below code or create your own. +# Deploy with `firebase deploy` + +from firebase_functions import https_fn +from firebase_admin import initialize_app + +# initialize_app() +# +# +# @https_fn.on_request() +# def on_request_example(req: https_fn.Request) -> https_fn.Response: +# return https_fn.Response("Hello world!") \ No newline at end of file diff --git a/templates/init/functions/python/requirements.txt b/templates/init/functions/python/requirements.txt new file mode 100644 index 00000000000..bcf4c12d317 --- /dev/null +++ b/templates/init/functions/python/requirements.txt @@ -0,0 +1 @@ +firebase_functions~=0.1.0 \ No newline at end of file diff --git a/templates/init/functions/typescript/_eslintrc b/templates/init/functions/typescript/_eslintrc index ff21277afae..0f8e2a9b7e8 100644 --- a/templates/init/functions/typescript/_eslintrc +++ b/templates/init/functions/typescript/_eslintrc @@ -19,6 +19,7 @@ module.exports = { }, ignorePatterns: [ "/lib/**/*", // Ignore built files. + "/generated/**/*", // Ignore generated files. ], plugins: [ "@typescript-eslint", @@ -27,5 +28,6 @@ module.exports = { rules: { "quotes": ["error", "double"], "import/no-unresolved": 0, + "indent": ["error", 2], }, }; diff --git a/templates/init/functions/typescript/_gitignore b/templates/init/functions/typescript/_gitignore index 65b4c06ecf8..9be0f014f4e 100644 --- a/templates/init/functions/typescript/_gitignore +++ b/templates/init/functions/typescript/_gitignore @@ -7,3 +7,4 @@ typings/ # Node.js dependency directory node_modules/ +*.local \ No newline at end of file diff --git a/templates/init/functions/typescript/index.ts b/templates/init/functions/typescript/index.ts index 10c30843a62..9462d2c77ed 100644 --- a/templates/init/functions/typescript/index.ts +++ b/templates/init/functions/typescript/index.ts @@ -1,9 +1,19 @@ -import * as functions from "firebase-functions"; +/** + * Import function triggers from their respective submodules: + * + * import {onCall} from "firebase-functions/v2/https"; + * import {onDocumentWritten} from "firebase-functions/v2/firestore"; + * + * See a full list of supported triggers at https://firebase.google.com/docs/functions + */ -// // Start writing Firebase Functions -// // https://firebase.google.com/docs/functions/typescript -// -// export const helloWorld = functions.https.onRequest((request, response) => { -// functions.logger.info("Hello logs!", {structuredData: true}); +import {onRequest} from "firebase-functions/v2/https"; +import * as logger from "firebase-functions/logger"; + +// Start writing functions +// https://firebase.google.com/docs/functions/typescript + +// export const helloWorld = onRequest((request, response) => { +// logger.info("Hello logs!", {structuredData: true}); // response.send("Hello from Firebase!"); // }); diff --git a/templates/init/functions/typescript/package.lint.json b/templates/init/functions/typescript/package.lint.json index 79d96a6bd58..b7acb3bbcb9 100644 --- a/templates/init/functions/typescript/package.lint.json +++ b/templates/init/functions/typescript/package.lint.json @@ -3,6 +3,7 @@ "scripts": { "lint": "eslint --ext .js,.ts .", "build": "tsc", + "build:watch": "tsc --watch", "serve": "npm run build && firebase emulators:start --only functions", "shell": "npm run build && firebase functions:shell", "start": "npm run shell", @@ -10,21 +11,21 @@ "logs": "firebase functions:log" }, "engines": { - "node": "16" + "node": "18" }, "main": "lib/index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^3.9.1", - "@typescript-eslint/parser": "^3.8.0", - "eslint": "^7.6.0", + "@typescript-eslint/eslint-plugin": "^5.12.0", + "@typescript-eslint/parser": "^5.12.0", + "eslint": "^8.9.0", "eslint-config-google": "^0.14.0", - "eslint-plugin-import": "^2.22.0", - "firebase-functions-test": "^0.2.0", - "typescript": "^3.8.0" + "eslint-plugin-import": "^2.25.4", + "firebase-functions-test": "^3.1.0", + "typescript": "^4.9.0" }, "private": true } diff --git a/templates/init/functions/typescript/package.nolint.json b/templates/init/functions/typescript/package.nolint.json index 35137e1042c..e27e3f2f880 100644 --- a/templates/init/functions/typescript/package.nolint.json +++ b/templates/init/functions/typescript/package.nolint.json @@ -2,6 +2,7 @@ "name": "functions", "scripts": { "build": "tsc", + "build:watch": "tsc --watch", "serve": "npm run build && firebase emulators:start --only functions", "shell": "npm run build && firebase functions:shell", "start": "npm run shell", @@ -9,16 +10,16 @@ "logs": "firebase functions:log" }, "engines": { - "node": "16" + "node": "18" }, "main": "lib/index.js", "dependencies": { - "firebase-admin": "^9.8.0", - "firebase-functions": "^3.14.1" + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" }, "devDependencies": { - "typescript": "^3.8.0", - "firebase-functions-test": "^0.2.0" + "typescript": "^4.9.0", + "firebase-functions-test": "^3.1.0" }, "private": true -} +} \ No newline at end of file diff --git a/templates/init/storage/storage.rules b/templates/init/storage/storage.rules index 4eda34fdf9b..f08744f032e 100644 --- a/templates/init/storage/storage.rules +++ b/templates/init/storage/storage.rules @@ -1,8 +1,12 @@ rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { - allow read, write: if request.auth!=null; + allow read, write: if false; } } } diff --git a/templates/popup.html b/templates/popup.html new file mode 100644 index 00000000000..8e14fa0e109 --- /dev/null +++ b/templates/popup.html @@ -0,0 +1,64 @@ + + + + + + + diff --git a/tsconfig.json b/tsconfig.json index ab04983ca2e..1cb63de99d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,11 @@ "src/types" ] }, - "include": ["src/**/*"] + "lib": [ + "dom", + "dom.iterable", + "es2019" + ], + "include": ["src/**/*"], + "exclude": ["src/dynamicImport.js"] } diff --git a/tsconfig.publish.json b/tsconfig.publish.json index 06b8de116b6..59fc1a12d0f 100644 --- a/tsconfig.publish.json +++ b/tsconfig.publish.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "sourceMap": false, + "sourceMap": false }, - "exclude": ["src/test/**/*"] + "exclude": ["src/**/*.spec.*", "src/**/testing/**/*", "src/test/**/*"] }